pi-repl-py 0.1.1 → 0.2.0
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 +21 -22
- package/docs/ARCHITECTURE.md +128 -88
- package/docs/how-to-functions.md +82 -80
- package/docs/philosophy.md +67 -59
- package/index.ts +4 -22
- package/package.json +5 -6
- package/scripts/setup-venv.mjs +39 -17
- package/src/engine/index.ts +90 -424
- package/src/engine/kernel.ts +598 -0
- package/src/engine/session.ts +149 -0
- package/src/engine/zmtp.ts +251 -0
- package/src/extension/helpers.ts +46 -0
- package/src/extension/preview/candidates.ts +7 -53
- package/src/extension/preview/descriptor.ts +2 -2
- package/src/extension/preview/index.ts +3 -11
- package/src/extension/preview/shell.ts +4 -4
- package/src/extension/preview/types.ts +1 -1
- package/src/extension/prompt.ts +67 -45
- package/src/extension/render-core.ts +5 -9
- package/src/extension/render.ts +2 -11
- package/src/extension/session-engine.ts +9 -31
- package/src/extension/tool-meta.ts +6 -6
- package/src/engine/guest.py +0 -320
- package/src/engine/protocol.ts +0 -66
- package/src/engine/toolbox/bash.py +0 -72
- package/src/engine/toolbox/edit.py +0 -37
- package/src/engine/toolbox/read.py +0 -26
- package/src/engine/toolbox/write.py +0 -23
- package/src/extension/config.ts +0 -64
- package/src/extension/preview-core.ts +0 -2
- package/src/extension/toolbox.ts +0 -98
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ the kernel stayed alive.
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
# from a clone, one-time setup
|
|
24
|
-
just setup # npm install + a project-local .venv with
|
|
24
|
+
just setup # npm install + a project-local .venv with ipykernel
|
|
25
25
|
|
|
26
26
|
# run a session
|
|
27
27
|
pi --repl
|
|
@@ -34,49 +34,48 @@ A plain `pi` session is untouched; the extension is dormant until `--repl` is pa
|
|
|
34
34
|
|
|
35
35
|
`npm install` runs a `postinstall` that creates the Python venv the evaluator needs, at a stable
|
|
36
36
|
per-user path (`~/.pi/agent/pi-repl/venv`). If `python3` or the network is missing, it prints a
|
|
37
|
-
clear notice. How the interpreter is resolved is in [docs/
|
|
37
|
+
clear notice. How the interpreter is resolved is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
38
38
|
|
|
39
39
|
## What you get
|
|
40
40
|
|
|
41
41
|
- **A persistent namespace.** Variables, functions, imports, and data survive across cells and
|
|
42
42
|
turns; snapshots preserve them across a best-effort restart.
|
|
43
43
|
- **A real `ipython` kernel**, not a hand-rolled `exec` loop.
|
|
44
|
-
- **Shell as
|
|
45
|
-
|
|
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.
|
|
46
47
|
- **Error survival.** A cell that throws reports the traceback and the kernel keeps going.
|
|
47
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.
|
|
48
49
|
|
|
49
|
-
##
|
|
50
|
+
## Helpers
|
|
50
51
|
|
|
51
|
-
A
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
A **helper** is a Python function you preload into every kernel. Drop a file in the one
|
|
53
|
+
helpers directory, restart the session, and the function is callable from the workspace —
|
|
54
|
+
for example, `helpers/double.py` exposing `def double` becomes `double(...)`. It ships
|
|
55
|
+
**empty** (shell and file IO are already plain Python), so a fresh install preloads nothing
|
|
56
|
+
until you add one. Each helper's `helper_description` is shown to the model verbatim;
|
|
57
|
+
the full contract lives in [docs/how-to-functions.md](docs/how-to-functions.md).
|
|
56
58
|
|
|
57
59
|
Everything the extension keeps lives under one folder in your home directory:
|
|
58
60
|
|
|
59
61
|
```
|
|
60
62
|
~/.pi/agent/pi-repl/
|
|
61
|
-
config.json settings (toolboxDir, pythonPath, timeoutMs)
|
|
62
63
|
venv/ the Python interpreter + ipykernel
|
|
63
|
-
|
|
64
|
+
helpers/ your helpers (created empty on install; every *.py loads)
|
|
64
65
|
state/ per-session namespace snapshots
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
The helpers directory is fixed at `~/.pi/agent/pi-repl/helpers` — no config file.
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
the kernel also only loads the toolbox at boot.
|
|
73
|
-
|
|
74
|
-
- Adding a function (the file contract, docstrings, disabling): [docs/how-to-functions.md](docs/how-to-functions.md)
|
|
70
|
+
Changing a helper (adding/removing a file, renaming one with a `_` prefix) needs a
|
|
71
|
+
**session restart / `/reload`**: the prompt list is built when `execute` is registered and
|
|
72
|
+
the kernel execs helpers only at boot.
|
|
75
73
|
|
|
76
74
|
## Configuration
|
|
77
75
|
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
There is deliberately no configuration file. Everything is arranged under
|
|
77
|
+
`~/.pi/agent/pi-repl/`: the venv, the fixed helpers dir, and the session state.
|
|
78
|
+
The Python interpreter is auto-resolved (the venv, else `$PYTHON`/`python3`).
|
|
80
79
|
|
|
81
80
|
## More
|
|
82
81
|
|
|
@@ -86,7 +85,7 @@ Full keys and path rules: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
|
86
85
|
## It is not
|
|
87
86
|
|
|
88
87
|
- A sandbox. The kernel runs with your permissions; the toolbox trusts you.
|
|
89
|
-
- A subagent framework. There
|
|
88
|
+
- A subagent framework. There is no `repl.run` API; spawn a process with `subprocess.run`.
|
|
90
89
|
- A pi tool-rack. It is one `execute` tool with functions inside.
|
|
91
90
|
|
|
92
91
|
## License
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,142 +1,182 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
6
|
|
|
5
7
|
```
|
|
6
8
|
pi
|
|
7
|
-
└─ extension (index.ts)
|
|
8
|
-
└─ EngineManager (src/engine/index.ts)
|
|
9
|
-
│
|
|
10
|
-
│
|
|
11
|
-
└─
|
|
9
|
+
└─ extension (index.ts) registers `execute`; dormant until --repl
|
|
10
|
+
└─ EngineManager (src/engine/index.ts) venv resolution, lazy spawn,
|
|
11
|
+
│ the call queue, snapshots,
|
|
12
|
+
│ abort grace, teardown
|
|
13
|
+
└─ KernelClient (src/engine/kernel.ts) one ipykernel subprocess
|
|
14
|
+
├─ ZMTP 3.0 (src/engine/zmtp.ts) the wire protocol, by hand
|
|
15
|
+
├─ Jupyter session (src/engine/session.ts) framing + HMAC + JSON
|
|
16
|
+
└─ python -m ipykernel -f <connection-file> the evaluator
|
|
12
17
|
```
|
|
13
18
|
|
|
14
|
-
The host is TypeScript; the evaluator is Python in its own process.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
22
|
+
|
|
23
|
+
## Why the host speaks ZMTP itself
|
|
24
|
+
|
|
25
|
+
The obvious way for a TypeScript host to drive an `ipykernel` is to load a ZMQ client
|
|
26
|
+
library. That does not work here: libzmq's native Node bindings crash `bun`. So an earlier
|
|
27
|
+
design put a Python middleman (`guest.py`) between the host and the kernel, translating a
|
|
28
|
+
private JSON protocol over a file descriptor into the real Jupyter protocol.
|
|
29
|
+
|
|
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`).
|
|
33
|
+
The payoff:
|
|
34
|
+
|
|
35
|
+
- **one process boundary** instead of two;
|
|
36
|
+
- **one standard protocol** (Jupyter) instead of a private one on top of it;
|
|
37
|
+
- **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.
|
|
18
41
|
|
|
19
42
|
## The Python environment (the venv)
|
|
20
43
|
|
|
21
|
-
The evaluator is a real `
|
|
22
|
-
`ipykernel`
|
|
23
|
-
|
|
44
|
+
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
|
+
(`jupyter_client` is *not* needed: the host is the client.)
|
|
24
47
|
|
|
25
48
|
When installed as a pi package, `npm install` runs `postinstall`
|
|
26
|
-
(`scripts/setup-venv.mjs`), which
|
|
49
|
+
(`scripts/setup-venv.mjs`), which builds a stable per-user venv:
|
|
27
50
|
|
|
28
51
|
```
|
|
29
52
|
~/.pi/agent/pi-repl/venv/bin/python3
|
|
30
53
|
```
|
|
31
54
|
|
|
32
|
-
That path is stable across updates because it sits outside the
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
runtime.
|
|
55
|
+
That path is stable across updates because it sits outside the package's own directory,
|
|
56
|
+
which npm replaces on each update. If `python3` or the network is missing at install time,
|
|
57
|
+
`postinstall` prints a clear notice and the host falls back at runtime.
|
|
36
58
|
|
|
37
|
-
At spawn, `resolvePythonPath`
|
|
59
|
+
At spawn, `resolvePythonPath` picks the interpreter in this order:
|
|
38
60
|
|
|
39
61
|
1. the repo's own `.venv` (development)
|
|
40
|
-
2. a
|
|
62
|
+
2. a venv in the current directory (per-project)
|
|
41
63
|
3. `~/.pi/agent/pi-repl/venv` (package install)
|
|
42
|
-
4. `$PYTHON
|
|
64
|
+
4. `$PYTHON`, then `python3` (the fallback)
|
|
65
|
+
|
|
66
|
+
The first one that exists wins. The tool's prompt tells the model it runs in a project-local
|
|
67
|
+
venv, not the system interpreter, so it does not leak the wrong assumption into commands.
|
|
68
|
+
|
|
69
|
+
## The kernel client
|
|
43
70
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
71
|
+
`KernelClient.start` spawns `python -m ipykernel -f <connection-file>` with a per-run
|
|
72
|
+
connection file in the temp directory, connects the three channels as ZMTP sockets, and
|
|
73
|
+
waits for a `kernel_info_reply` before declaring the kernel ready. Cells run as standard
|
|
74
|
+
Jupyter `execute_request`s, routed by `msg_id`:
|
|
47
75
|
|
|
48
|
-
|
|
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.
|
|
78
|
+
- **shell** carries the authoritative `execute_reply` (status, ename, evalue).
|
|
79
|
+
- **control** carries interrupts (`interrupt_request`) and shutdown.
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
`ipykernel` subprocess (`python -m ipykernel`), keeps a blocking client
|
|
52
|
-
attached, and stays alive for the whole session. Cells run in that kernel via
|
|
53
|
-
`kc.execute(code)`, so state persists because the kernel process does.
|
|
81
|
+
Two wire subtleties are load-bearing, and both are pinned by the contract tests.
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
**A cell is not done until two things arrive.** The shell reply and the iopub output stream
|
|
84
|
+
travel on different connections, so a tiny reply can arrive before a large output has
|
|
85
|
+
finished draining on iopub. A cell settles only when **both** the `execute_reply` and the
|
|
86
|
+
matching iopub `status idle` (published after every byte of output) have arrived. Settling
|
|
87
|
+
on the reply alone would drop output that was still in flight.
|
|
56
88
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
**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.
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
nothing.
|
|
94
|
+
**Cancellation is real.** An abort sends an `interrupt_request` on the control channel,
|
|
95
|
+
which raises a genuine `KeyboardInterrupt` in the running cell; the namespace survives. As a
|
|
96
|
+
backstop for cells wedged in C code (which ignore interrupts), the engine's abort grace then
|
|
97
|
+
kills the kernel after 500 ms, and the next call rebuilds it from the last snapshot.
|
|
66
98
|
|
|
67
|
-
##
|
|
99
|
+
## Helpers loading
|
|
68
100
|
|
|
69
|
-
At boot the
|
|
70
|
-
|
|
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.
|
|
71
104
|
|
|
72
|
-
- **
|
|
105
|
+
- **The kernel** execs each `*.py` into its namespace, so the file's functions become
|
|
73
106
|
callable.
|
|
74
|
-
- **host** reads the same files to build the
|
|
75
|
-
|
|
76
|
-
real signatures and one-line summaries.
|
|
107
|
+
- **The host** reads the same files to build the helper list shown in the `execute` tool's
|
|
108
|
+
prompt, so the model sees each `helper_description` verbatim.
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`docs/how-to-functions.md`.
|
|
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
|
|
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.
|
|
84
115
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
to be reflected in the prompt — the kernel also loads the toolbox only at boot.
|
|
116
|
+
There are no custom discovery intrinsics (`ls()` / `help()`) injected into a bare kernel.
|
|
117
|
+
The model discovers what is loaded by listing the namespace with ordinary Python:
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
```python
|
|
120
|
+
[k for k in globals() if not k.startswith('_')]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For the full helper contract — the description, the docstring, disabling — see
|
|
124
|
+
[how-to-functions.md](how-to-functions.md).
|
|
91
125
|
|
|
92
126
|
## Snapshots & honest resets
|
|
93
127
|
|
|
94
|
-
After each successful cell the host schedules a debounced snapshot
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(live handles, some objects) is reported by name.
|
|
128
|
+
After each successful cell, the host schedules a debounced snapshot. A private kernel cell
|
|
129
|
+
pickles the kernel's `globals` (entry by entry, so one value that cannot be pickled costs
|
|
130
|
+
only itself) and publishes the result back over a private MIME payload. The host stores it as
|
|
131
|
+
`namespace.snapshot`, keyed to the session file under
|
|
132
|
+
`~/.pi/agent/pi-repl/state/<session>/`.
|
|
100
133
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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,
|
|
136
|
+
the result is prefixed with a `<repl_engine_reset>` block that names what was revived and
|
|
137
|
+
what was lost, so the model re-verifies before reusing state that may be gone.
|
|
104
138
|
|
|
105
139
|
## Failure modes
|
|
106
140
|
|
|
107
141
|
| Failure | Behaviour |
|
|
108
142
|
| --- | --- |
|
|
109
|
-
| Cell throws | `
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
| Host exits |
|
|
143
|
+
| Cell throws | `error` status with traceback; kernel namespace intact |
|
|
144
|
+
| Cell silent or wedged | the watchdog sends an `interrupt_request`; a caller abort then kills the kernel after a 500 ms grace |
|
|
145
|
+
| Kernel dies | the running cell settles with an error; the next call builds a fresh kernel and restores the last snapshot |
|
|
146
|
+
| Host exits | `process.on("exit")` SIGKILLs live kernels (a child does not die with its parent) |
|
|
113
147
|
| Output flood | capped per channel, truncation announced |
|
|
114
148
|
|
|
115
149
|
## Testing
|
|
116
150
|
|
|
117
|
-
- **Host (
|
|
118
|
-
`test/preview-core.test.ts
|
|
119
|
-
- **
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
151
|
+
- **Host (fast):** `test/units.test.ts` covers engine orchestration, rendering, and config;
|
|
152
|
+
`test/preview-core.test.ts` covers the preview logic.
|
|
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.
|
|
157
|
+
|
|
158
|
+
The gate is `just check` — biome (format + lint) plus the host tests. `just integration`
|
|
159
|
+
adds the real-kernel suite.
|
|
123
160
|
|
|
124
|
-
|
|
125
|
-
`just integration` adds the real-host seam.
|
|
161
|
+
## The fixed layout
|
|
126
162
|
|
|
127
|
-
|
|
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:
|
|
128
165
|
|
|
129
|
-
|
|
130
|
-
|
|
166
|
+
```
|
|
167
|
+
~/.pi/agent/pi-repl/
|
|
168
|
+
venv/ the Python interpreter + ipykernel
|
|
169
|
+
helpers/ the helpers directory — every *.py loads into every kernel
|
|
170
|
+
state/ per-session namespace snapshots
|
|
171
|
+
```
|
|
131
172
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
| `snapshotDebounceMs` | number, 1500 | Debounce after an ok cell before snapshot, in ms. |
|
|
173
|
+
The helpers directory is fixed at `~/.pi/agent/pi-repl/helpers` (matching the kernel's
|
|
174
|
+
`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
|
+
needed. The per-cell silence watchdog is off by default (`PI_REPL_TIMEOUT_MS=0`: a silent
|
|
177
|
+
but working cell may run on).
|
|
138
178
|
|
|
139
179
|
## Reference documentation
|
|
140
180
|
|
|
141
|
-
- Philosophy and design rationale: [
|
|
142
|
-
- Adding a
|
|
181
|
+
- Philosophy and design rationale: [philosophy.md](philosophy.md)
|
|
182
|
+
- Adding a helper: [how-to-functions.md](how-to-functions.md)
|
package/docs/how-to-functions.md
CHANGED
|
@@ -1,108 +1,110 @@
|
|
|
1
|
-
# How to
|
|
1
|
+
# How to write a helper
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A **helper** is a Python function you write once that becomes available to the agent in
|
|
4
|
+
every `pi --repl` session. You drop a `.py` file into one folder, restart the session, and
|
|
5
|
+
the function is callable from the workspace — like a bookmark for code the agent keeps
|
|
6
|
+
reaching for.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
> toolbox change (add/remove a file, rename one with a `_` prefix) is picked up
|
|
11
|
-
> by a **session restart / `/reload`** — not mid-session.
|
|
8
|
+
This guide shows the smallest helper that works, then explains the three parts every
|
|
9
|
+
helper file has and the habits that make a helper useful.
|
|
12
10
|
|
|
13
|
-
##
|
|
11
|
+
## Prerequisites
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
config:
|
|
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.
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
// ~/.pi/agent/pi-repl/config.json
|
|
21
|
-
{ "toolboxDir": "~/.pi/agent/pi-repl/functions" }
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Use an absolute path or a `~`-prefixed one (`~` expands to your home). A bare relative
|
|
25
|
-
path resolves from the process working directory, which is not reliable, so prefer
|
|
26
|
-
an absolute path for a stable per-user folder. Point `toolboxDir` at a directory and
|
|
27
|
-
every `*.py` there is loaded **in addition to** the shipped `read`/`write`/`edit`/`bash`.
|
|
28
|
-
If a file in your folder has the **same name** as a built-in (e.g. `read.py`), your
|
|
29
|
-
version wins and the built-in is ignored for that name.
|
|
16
|
+
## The smallest helper
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
Create this file:
|
|
32
19
|
|
|
33
|
-
|
|
20
|
+
```python
|
|
21
|
+
# ~/.pi/agent/pi-repl/helpers/double.py
|
|
22
|
+
helper_description = """double(x) — multiply a value by two."""
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
24
|
+
def double(x):
|
|
25
|
+
"""Return x * 2. Works on ints, floats, and lists."""
|
|
26
|
+
return x * 2
|
|
27
|
+
```
|
|
38
28
|
|
|
39
|
-
|
|
29
|
+
Restart the session (`/reload`, or relaunch `pi --repl`), then check it loaded:
|
|
40
30
|
|
|
41
31
|
```python
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
__all__ = ["summarize"]
|
|
32
|
+
print([k for k in globals() if not k.startswith('_')])
|
|
33
|
+
# ['double', ...]
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
print(double(21))
|
|
36
|
+
# 42
|
|
49
37
|
```
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
If `double` appears in the namespace and runs, it's loaded. That is the whole loop: write
|
|
40
|
+
the file, restart, use it.
|
|
53
41
|
|
|
54
|
-
##
|
|
42
|
+
## How it actually works
|
|
55
43
|
|
|
56
|
-
|
|
57
|
-
Arguments are read from the actual `def` line rather than hand-copied into a
|
|
58
|
-
docstring, so the signature the model sees tracks the code for a normal
|
|
59
|
-
single-line signature. Change `def summarize(text,
|
|
60
|
-
limit=1):` to `limit=200`, and after the next session restart the prompt
|
|
61
|
-
updates to match.
|
|
44
|
+
Two things happen with the file you wrote:
|
|
62
45
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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*).
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
`
|
|
70
|
-
|
|
71
|
-
guidance (only the one-line `function_description` does). Keep it for
|
|
72
|
-
details, the venv note, and edge cases.
|
|
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.
|
|
73
54
|
|
|
74
|
-
|
|
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.
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
good `function_description` is one line ("Run a shell command and return its
|
|
78
|
-
result."). A good docstring explains arguments, return value, and any
|
|
79
|
-
non-obvious behavior, including environment facts the model needs
|
|
80
|
-
("the evaluator runs in a project-local venv, not the system python").
|
|
60
|
+
## The three parts of a helper file
|
|
81
61
|
|
|
82
|
-
|
|
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 |
|
|
83
67
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
it never reaches the kernel or the model. Use this for scratch or internal
|
|
87
|
-
helpers.
|
|
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.
|
|
88
70
|
|
|
89
|
-
|
|
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):
|
|
90
74
|
|
|
91
|
-
-
|
|
92
|
-
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
docstring so the model knows the trade-off.
|
|
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.`
|
|
96
79
|
|
|
97
|
-
|
|
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.
|
|
98
84
|
|
|
99
|
-
|
|
85
|
+
## Common mistakes
|
|
100
86
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
97
|
+
|
|
98
|
+
## Disable a file without deleting it
|
|
99
|
+
|
|
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.
|
|
102
|
+
|
|
103
|
+
## Checklist
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|