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/docs/philosophy.md
CHANGED
|
@@ -2,87 +2,95 @@
|
|
|
2
2
|
|
|
3
3
|
## The bet
|
|
4
4
|
|
|
5
|
-
Most coding agents
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Most coding agents carry a toolbox of point tools: a read tool, a bash tool, an edit tool,
|
|
6
|
+
a search tool, each with its own schema, its own failure modes, and its own token cost to
|
|
7
|
+
describe. The model spends context deciding *which* tool to call, then *how* to thread one
|
|
8
|
+
tool's output into the next.
|
|
9
9
|
|
|
10
|
-
pi-repl makes the opposite bet: **give the model one persistent Python
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
17
17
|
|
|
18
18
|
## What persistence buys
|
|
19
19
|
|
|
20
|
-
A
|
|
21
|
-
agent pastes it
|
|
22
|
-
transformation is round-tripped through the transcript and billed as tokens.
|
|
20
|
+
A point-tool loop re-parses text at every step. The `read` tool returns a string, so the
|
|
21
|
+
agent pastes it back into context. The `grep` tool returns lines, so the agent re-reads
|
|
22
|
+
them. Every transformation is round-tripped through the transcript and billed as tokens.
|
|
23
23
|
|
|
24
|
-
In a persistent kernel
|
|
24
|
+
In a persistent kernel that work happens once and stays put:
|
|
25
25
|
|
|
26
|
-
- a variable assigned in one cell is there in the next, and the next turn;
|
|
26
|
+
- a variable assigned in one cell is still there in the next cell, and the next turn;
|
|
27
27
|
- a function defined once is reusable for the whole session;
|
|
28
|
-
- `
|
|
29
|
-
|
|
30
|
-
normal code.
|
|
28
|
+
- `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.
|
|
31
30
|
|
|
32
|
-
The savings compound
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
the current step needs.
|
|
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,
|
|
33
|
+
and store in code, printing only what the current step needs.
|
|
36
34
|
|
|
37
35
|
## Why a real kernel
|
|
38
36
|
|
|
39
|
-
pi-repl does not hand-roll an `exec` loop. It drives a genuine `ipython`
|
|
40
|
-
|
|
37
|
+
pi-repl does not hand-roll an `exec` loop. It drives a genuine `ipython` kernel in a
|
|
38
|
+
separate process. That buys four things a script string passed to `exec` cannot give:
|
|
41
39
|
|
|
42
|
-
- rich, real tracebacks instead of a wrapped `except`;
|
|
43
|
-
-
|
|
44
|
-
- last-expression capture;
|
|
45
|
-
- a namespace that
|
|
46
|
-
|
|
40
|
+
- **rich, real tracebacks** instead of a wrapped `except`;
|
|
41
|
+
- **real interrupts** — a stuck cell can be interrupted mid-run without losing the session;
|
|
42
|
+
- **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.
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
notice
|
|
46
|
+
It is also an honest isolation boundary. The kernel is its own process, not part of pi. A
|
|
47
|
+
cell that raises leaves pi answering and the namespace intact, because pi is not the process
|
|
48
|
+
that failed. A cell that wedges the *whole* kernel instead stops cells from running until
|
|
49
|
+
the next call notices the dead kernel and rebuilds it from the last completed snapshot.
|
|
50
|
+
Either way the result carries a `<repl_engine_reset>` notice that names what the rebuild
|
|
51
|
+
revived and what it lost, so the model re-verifies before trusting state that may be gone.
|
|
52
|
+
(How that machinery works is in `ARCHITECTURE.md`.)
|
|
53
53
|
|
|
54
54
|
## The venv as part of the design
|
|
55
55
|
|
|
56
|
-
Because the evaluator is real Python, it needs a real Python environment with
|
|
57
|
-
|
|
56
|
+
Because the evaluator is real Python, it needs a real Python environment with `ipykernel`.
|
|
57
|
+
You cannot conjure that from a script; it is a hard runtime dependency.
|
|
58
58
|
|
|
59
|
-
The package's `postinstall` creates it once, at a stable user path
|
|
60
|
-
(`~/.pi/agent/pi-repl/venv`), so a `pi install` ends with a working evaluator
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
The package's `postinstall` creates it once, at a stable per-user path
|
|
60
|
+
(`~/.pi/agent/pi-repl/venv`), so a `pi install` normally ends with a working evaluator. If
|
|
61
|
+
`python3` or the network is missing at install time, `postinstall` prints a clear notice and
|
|
62
|
+
the host falls back to `$PYTHON` or `python3` at runtime. Updates never lose it, because the
|
|
63
|
+
venv lives outside the ephemeral package directory where it would vanish on every update.
|
|
64
|
+
|
|
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.
|
|
68
76
|
|
|
69
77
|
## Trust, not a sandbox
|
|
70
78
|
|
|
71
|
-
This is deliberately **not
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
This is deliberately **not a sandbox.** The kernel runs with your user's permissions, can
|
|
80
|
+
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.
|
|
76
84
|
|
|
77
85
|
## What it isn't
|
|
78
86
|
|
|
79
|
-
- A subagent framework
|
|
80
|
-
|
|
81
|
-
- A
|
|
82
|
-
|
|
83
|
-
- A replacement for your own editing
|
|
84
|
-
|
|
87
|
+
- **A subagent framework.** There is no `repl.run`. To delegate, the model spawns a process
|
|
88
|
+
with `subprocess.run` (or `!cmd` / `%%bash`).
|
|
89
|
+
- **A pi tool-rack.** It exposes one `execute` tool; everything else lives inside that
|
|
90
|
+
workspace.
|
|
91
|
+
- **A replacement for your own editing and browsing tools.** It is there when the working
|
|
92
|
+
style above is worth it, and dormant otherwise.
|
|
85
93
|
|
|
86
|
-
The trade-off is real and accepted: the agent pays a little more per
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
The trade-off is real and accepted: the agent pays a little more per call to hold a heavier
|
|
95
|
+
environment, and gets back far fewer re-reads, fewer transcript round-trips, and sharper
|
|
96
|
+
small-model behaviour.
|
package/index.ts
CHANGED
|
@@ -4,8 +4,7 @@ import { basename, join } from "node:path";
|
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import {
|
|
8
|
-
import { loadConfig } from "./src/extension/config.js";
|
|
7
|
+
import { EngineManager } from "./src/engine/index.js";
|
|
9
8
|
import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
|
|
10
9
|
import { EngineLifecycle, summarizeNames } from "./src/extension/session-engine.js";
|
|
11
10
|
import { EXECUTE_DESCRIPTION, buildExecutePromptGuidelines, EXECUTE_PROMPT_SNIPPET } from "./src/extension/tool-meta.js";
|
|
@@ -45,8 +44,6 @@ function composeErrorLines(error: { name: string; message: string; stack: string
|
|
|
45
44
|
return stack[0]?.trim() === header ? stack : [header, ...stack];
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const CFG = loadConfig();
|
|
49
|
-
|
|
50
47
|
export default function (pi: ExtensionAPI) {
|
|
51
48
|
pi.registerFlag("repl", {
|
|
52
49
|
type: "boolean",
|
|
@@ -67,9 +64,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
67
64
|
const stateDir = join(homedir(), ".pi", "agent", "pi-repl", "state", sessionKey ?? "ephemeral");
|
|
68
65
|
return new EngineManager({
|
|
69
66
|
cwd,
|
|
70
|
-
pythonPath: CFG.pythonPath,
|
|
71
|
-
timeoutMs: CFG.timeoutMs,
|
|
72
|
-
toolboxDir: CFG.toolboxDir,
|
|
73
67
|
// --- snapshots are keyed to a session file; ephemeral sessions get none ---
|
|
74
68
|
snapshot: sessionKey ? { path: join(stateDir, "namespace.snapshot") } : undefined,
|
|
75
69
|
});
|
|
@@ -130,7 +124,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
124
|
label: "execute",
|
|
131
125
|
description: EXECUTE_DESCRIPTION,
|
|
132
126
|
promptSnippet: EXECUTE_PROMPT_SNIPPET,
|
|
133
|
-
promptGuidelines: buildExecutePromptGuidelines(
|
|
127
|
+
promptGuidelines: buildExecutePromptGuidelines(),
|
|
134
128
|
parameters: executeSchema,
|
|
135
129
|
renderShell: "self",
|
|
136
130
|
renderCall(args, theme, context) {
|
|
@@ -189,22 +183,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
183
|
pendingErrorResults.set(toolCallId, { details });
|
|
190
184
|
throw new Error(text || "(no output)");
|
|
191
185
|
}
|
|
192
|
-
|
|
193
|
-
// --- a busy kernel can't be interrupted mid-cell; discard+rebuild so the next run doesn't queue ---
|
|
194
|
-
await lifecycle.discard();
|
|
195
|
-
}
|
|
186
|
+
// --- an aborted cell was interrupted, not wedged: the kernel keeps running, nothing to discard ---
|
|
196
187
|
return result;
|
|
197
188
|
} catch (error) {
|
|
198
|
-
|
|
199
|
-
// --- discard the wedged engine; the next cell revives from the last snapshot ---
|
|
200
|
-
await lifecycle.discard();
|
|
201
|
-
throw new Error(
|
|
202
|
-
"The evaluator was wedged by a previously interrupted cell and has been killed. " +
|
|
203
|
-
"Run the next cell to get a fresh evaluator revived from the last snapshot; " +
|
|
204
|
-
"anything newer than that snapshot is gone, so re-verify variables before reusing them.",
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
// --- a guest that died leaves the engine shutdown; drop it so the next cell rebuilds fresh ---
|
|
189
|
+
// --- a kernel that died (or was killed as the abort backstop) is down; drop it so the next cell rebuilds ---
|
|
208
190
|
if (m.isRunning === false) {
|
|
209
191
|
await lifecycle.discard();
|
|
210
192
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-repl-py",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
16
|
-
"url": "https://github.com/k3-2o/pi-repl-py.git"
|
|
16
|
+
"url": "git+https://github.com/k3-2o/pi-repl-py.git"
|
|
17
17
|
},
|
|
18
18
|
"homepage": "https://github.com/k3-2o/pi-repl-py",
|
|
19
19
|
"bugs": {
|
|
@@ -35,14 +35,13 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=22"
|
|
37
37
|
},
|
|
38
|
-
"scripts": {
|
|
38
|
+
"scripts": {
|
|
39
39
|
"typecheck": "tsc --noEmit",
|
|
40
40
|
"format": "biome format --write .",
|
|
41
41
|
"lint": "biome check .",
|
|
42
42
|
"knip": "knip",
|
|
43
|
-
"check": "
|
|
43
|
+
"check": "biome check . && npm run test:ts",
|
|
44
44
|
"test:ts": "bun test test/units.test.ts test/preview-core.test.ts",
|
|
45
|
-
"test:py": ".venv/bin/python -m pytest test/guest_contract.py -q",
|
|
46
45
|
"test:integ": "bun test test/engine.integration.test.ts",
|
|
47
46
|
"postinstall": "node scripts/setup-venv.mjs"
|
|
48
47
|
},
|
|
@@ -61,4 +60,4 @@
|
|
|
61
60
|
"typebox": "^1.3.11",
|
|
62
61
|
"typescript": "^5.6.0"
|
|
63
62
|
}
|
|
64
|
-
}
|
|
63
|
+
}
|
package/scripts/setup-venv.mjs
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* postinstall: build the stable per-user Python venv the
|
|
3
|
+
* postinstall: build the stable per-user Python venv the evaluator needs.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
* `ipykernel`
|
|
7
|
-
* is installed as a pi package there is no repo-local `.venv` (
|
|
8
|
-
* and excluded from the npm tarball), so we create one at a stable path
|
|
9
|
-
*
|
|
5
|
+
* The evaluator (src/engine/kernel.ts) drives a real ipykernel directly over
|
|
6
|
+
* the Jupyter protocol; `ipykernel` is the only hard runtime dependency. When
|
|
7
|
+
* this is installed as a pi package there is no repo-local `.venv` (gitignored
|
|
8
|
+
* and excluded from the npm tarball), so we create one at a stable path the
|
|
9
|
+
* engine also knows about:
|
|
10
10
|
*
|
|
11
11
|
* ~/.pi/agent/pi-repl/venv/bin/python3
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* HELPERS live ONLY in the user-owned config dir:
|
|
14
|
+
*
|
|
15
|
+
* ~/.pi/agent/pi-repl/helpers/
|
|
16
|
+
*
|
|
17
|
+
* There is no helper/config folder anywhere in this package (no
|
|
18
|
+
* src/engine/helpers, no templates/). The helpers dir is created if missing,
|
|
19
|
+
* but no default helpers are seeded — the REPL itself already provides shell
|
|
20
|
+
* (via `!cmd`, `%%bash`, `subprocess`) and file IO (via `open`, `pathlib`).
|
|
21
|
+
* Users add their own helper .py files freely; existing files are never clobbered.
|
|
22
|
+
*
|
|
23
|
+
* Failures are non-fatal: if there's no system python3 or no network we print a
|
|
24
|
+
* clear notice and let the engine fall back to '$PYTHON' or 'python3' at runtime.
|
|
16
25
|
*/
|
|
17
26
|
|
|
18
27
|
import { execSync } from "node:child_process";
|
|
@@ -22,13 +31,14 @@ import { join } from "node:path";
|
|
|
22
31
|
|
|
23
32
|
const VENV_DIR = join(homedir(), ".pi", "agent", "pi-repl", "venv");
|
|
24
33
|
const PY = join(VENV_DIR, "bin", "python3");
|
|
25
|
-
const DEPS = ["ipykernel"
|
|
34
|
+
const DEPS = ["ipykernel"];
|
|
35
|
+
const HELPERS_DIR = join(homedir(), ".pi", "agent", "pi-repl", "helpers");
|
|
26
36
|
|
|
27
|
-
function log(
|
|
28
|
-
process.stdout.write(`[pi-repl] ${
|
|
37
|
+
function log(m) {
|
|
38
|
+
process.stdout.write(`[pi-repl] ${m}\n`);
|
|
29
39
|
}
|
|
30
|
-
function warn(
|
|
31
|
-
process.stderr.write(`[pi-repl] warning: ${
|
|
40
|
+
function warn(m) {
|
|
41
|
+
process.stderr.write(`[pi-repl] warning: ${m}\n`);
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
function findSystemPython() {
|
|
@@ -41,8 +51,20 @@ function findSystemPython() {
|
|
|
41
51
|
return null;
|
|
42
52
|
}
|
|
43
53
|
|
|
54
|
+
// ---------------------------------------------------------------- helpers dir
|
|
55
|
+
// The helpers dir is user-owned. We create it empty on install. The REPL
|
|
56
|
+
// provides shell and file IO natively; helpers are for things the user adds
|
|
57
|
+
// themselves (e.g. web_search, custom skills). Existing files are never clobbered.
|
|
58
|
+
function seedHelpersDir() {
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(HELPERS_DIR, { recursive: true });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
warn(`could not create the helpers dir (${e?.message ?? e}); custom helpers won't preload.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
function main() {
|
|
45
|
-
|
|
67
|
+
seedHelpersDir();
|
|
46
68
|
if (existsSync(PY)) {
|
|
47
69
|
log(`venv already present at ${VENV_DIR}`);
|
|
48
70
|
return;
|
|
@@ -51,7 +73,7 @@ function main() {
|
|
|
51
73
|
if (!systemPython) {
|
|
52
74
|
warn(
|
|
53
75
|
`no python3 found on PATH; could not create the evaluator venv. ` +
|
|
54
|
-
`Install python3 and run '${PY.slice(-60)} -m venv' manually, or set
|
|
76
|
+
`Install python3 and run '${PY.slice(-60)} -m venv' manually, or set $PYTHON to point at one.`
|
|
55
77
|
);
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
@@ -64,7 +86,7 @@ function main() {
|
|
|
64
86
|
log("done. The pi-repl evaluator will use this venv.");
|
|
65
87
|
} catch (error) {
|
|
66
88
|
warn(`could not build the evaluator venv (${error && error.message ? error.message : error}). `);
|
|
67
|
-
warn("You
|
|
89
|
+
warn("You must install ipykernel in that venv before the evaluator runs.");
|
|
68
90
|
}
|
|
69
91
|
}
|
|
70
92
|
|