pi-repl-py 0.2.7 → 0.3.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 +1 -0
- package/docs/ARCHITECTURE.md +9 -7
- package/docs/helpers.md +20 -0
- package/index.ts +7 -17
- package/package.json +1 -1
- package/src/engine/index.ts +3 -8
- package/src/engine/kernel.ts +9 -2
- package/src/extension/prompt.ts +21 -44
- package/src/extension/session-engine.ts +1 -1
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ The Python interpreter is auto-resolved (the venv, else `$PYTHON`/`python3`).
|
|
|
96
96
|
- Why this design: [docs/design.md](docs/design.md)
|
|
97
97
|
- How it works, the venv, and the kernel: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
98
98
|
- How to write and load helpers: [docs/helpers.md](docs/helpers.md)
|
|
99
|
+
- Working examples you can copy: [example/](example/) — helpers under `example/helper/` and skills under `example/skills/`
|
|
99
100
|
- Termux / Android installation: [docs/termux.md](docs/termux.md)
|
|
100
101
|
|
|
101
102
|
## It is not
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -57,15 +57,17 @@ That path is stable across updates because it sits outside the package's own dir
|
|
|
57
57
|
which npm replaces on each update. If `python3` or the network is missing at install time,
|
|
58
58
|
`postinstall` prints a clear notice and the host falls back at runtime.
|
|
59
59
|
|
|
60
|
-
At spawn, `resolvePythonPath`
|
|
60
|
+
At spawn, `resolvePythonPath` uses exactly one interpreter, the install venv:
|
|
61
61
|
|
|
62
|
-
1.
|
|
63
|
-
2.
|
|
64
|
-
3. `~/.pi/agent/pi-repl/venv` (package install)
|
|
65
|
-
4. `$PYTHON`, then `python3` (the fallback)
|
|
62
|
+
1. `~/.pi/agent/pi-repl/venv` (the package install)
|
|
63
|
+
2. `$PYTHON`, then `python3` (only if the install venv is missing)
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
venv
|
|
65
|
+
It deliberately does NOT auto-pick the repo's or current directory's `.venv`: a per-project
|
|
66
|
+
venv is not guaranteed to have `ipykernel`, so preferring it (as earlier versions did) made
|
|
67
|
+
the kernel die from a `ModuleNotFoundError` whenever cwd happened to contain such a venv. The
|
|
68
|
+
kernel therefore always runs in the stable install environment. The kernel starts in the
|
|
69
|
+
session's cwd, falling back to the host's own cwd if that directory no longer exists (a
|
|
70
|
+
deleted project dir), so a stale cwd can never prevent the kernel from coming up.
|
|
69
71
|
|
|
70
72
|
## The kernel client
|
|
71
73
|
|
package/docs/helpers.md
CHANGED
|
@@ -119,6 +119,26 @@ the workspace.
|
|
|
119
119
|
|
|
120
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
121
|
|
|
122
|
+
## Third-party packages in the install venv
|
|
123
|
+
|
|
124
|
+
The evaluator runs from a real Python virtualenv (`~/.pi/agent/pi-repl/venv`); `sys.path` includes
|
|
125
|
+
its `site-packages`. Packages you install there are importable from helpers and from any cell:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
~/.pi/agent/pi-repl/venv/bin/pip3 install -U numpy pandas
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
import numpy as np
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
That is how a helper reaches a package the repl does not ship by default (the venv is created
|
|
136
|
+
minimal — it has no `requests`, `numpy`, `pandas`, etc.). ipykernel only pulls its own dependencies.
|
|
137
|
+
|
|
138
|
+
Because it is a project-local venv, the packages you add are personal to you: nothing about them
|
|
139
|
+
ships with pi-repl. A helper that depends on a package runs only on machines that have installed
|
|
140
|
+
it, so say so in the helper description or docstring when your helper imports one.
|
|
141
|
+
|
|
122
142
|
## Choosing what belongs in a helper
|
|
123
143
|
|
|
124
144
|
Write a helper when it owns a part of the work that is easy to get wrong or tedious to repeat:
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import { EngineManager } from "./src/engine/index.js";
|
|
8
8
|
import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
|
|
9
|
-
import { EngineLifecycle
|
|
9
|
+
import { EngineLifecycle } from "./src/extension/session-engine.js";
|
|
10
10
|
import { EXECUTE_DESCRIPTION, buildExecutePromptGuidelines, EXECUTE_PROMPT_SNIPPET } from "./src/extension/tool-meta.js";
|
|
11
11
|
|
|
12
12
|
const executeSchema = Type.Object({
|
|
@@ -87,23 +87,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
87
87
|
}
|
|
88
88
|
// --- active: the whole surface collapses to the one tool ---
|
|
89
89
|
pi.setActiveTools(["execute"]);
|
|
90
|
-
// ---
|
|
90
|
+
// --- warm the engine (and its revive) in the background; no popup. ---
|
|
91
|
+
// --- acquire() dedupes, so the first execute awaits this same in-flight boot ---
|
|
91
92
|
location = { cwd: ctx.cwd, sessionFile: ctx.sessionManager.getSessionFile() ?? undefined };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
content: `Revived ${restore.restored.length} variable(s) from the previous run: ${summarizeNames(restore.restored, 8)}${
|
|
97
|
-
restore.failed.length > 0
|
|
98
|
-
? `. Failed: ${summarizeNames(
|
|
99
|
-
restore.failed.map((f) => f.name),
|
|
100
|
-
8,
|
|
101
|
-
)}`
|
|
102
|
-
: ""
|
|
103
|
-
}`,
|
|
104
|
-
display: true,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
93
|
+
void lifecycle.acquire("startup").catch(() => {
|
|
94
|
+
// --- boot/revive handled on the execute path; swallow so a background warm can never
|
|
95
|
+
// --- surface an unhandled rejection and the model never needs the restore notice ---
|
|
96
|
+
});
|
|
107
97
|
});
|
|
108
98
|
|
|
109
99
|
pi.on("session_shutdown", async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-repl-py",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": [
|
package/src/engine/index.ts
CHANGED
|
@@ -5,21 +5,16 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
9
8
|
import { KernelClient } from "./kernel.js";
|
|
10
9
|
|
|
11
|
-
const GUEST_REL = fileURLToPath(new URL("./kernel.js", import.meta.url));
|
|
12
|
-
|
|
13
10
|
function installVenvPython(): string {
|
|
14
11
|
return join(homedir(), ".pi", "agent", "pi-repl", "venv", "bin", "python3");
|
|
15
12
|
}
|
|
16
13
|
|
|
17
14
|
/** Prefer a venv with ipykernel; else $PYTHON or python3. */
|
|
18
|
-
function resolvePythonPath(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const cwdVenv = cwd ? join(cwd, ".venv", "bin", "python3") : "";
|
|
22
|
-
if (cwdVenv && existsSync(cwdVenv)) return cwdVenv;
|
|
15
|
+
function resolvePythonPath(_cwd: string | undefined): string {
|
|
16
|
+
// Only ever use the install venv: a project or repo `.venv` may lack ipykernel and
|
|
17
|
+
// shadow the good environment, killing the kernel. No auto-picking.
|
|
23
18
|
const installVenv = installVenvPython();
|
|
24
19
|
if (existsSync(installVenv)) return installVenv;
|
|
25
20
|
return process.env.PYTHON ?? "python3";
|
package/src/engine/kernel.ts
CHANGED
|
@@ -54,7 +54,14 @@ export interface SnapshotReply {
|
|
|
54
54
|
// --- boot preload: exec each helper; ls()/help() are gone, discovery is globals() ---
|
|
55
55
|
|
|
56
56
|
/** Read the helpers dir (same skip rules as the extension's prompt loader). */
|
|
57
|
-
|
|
57
|
+
/** A directory for the kernel to start in; if the requested cwd is gone, fall back to the
|
|
58
|
+
* evaluator's own cwd rather than letting spawn() die with ENOENT. A deleted project dir is
|
|
59
|
+
* a real resume case (pi guards it too) — the kernel must still come up. */
|
|
60
|
+
function resolveCwd(requested?: string): string {
|
|
61
|
+
if (requested && existsSync(requested)) return requested;
|
|
62
|
+
return process.cwd();
|
|
63
|
+
}
|
|
64
|
+
function readHelperSources(dir?: string): { name: string; source: string }[] {
|
|
58
65
|
// --- one fixed dir, resolved like the prompt side (helpers.ts) so both always agree ---
|
|
59
66
|
const d = dir ?? join(homedir(), ".pi", "agent", "pi-repl", "helpers");
|
|
60
67
|
if (!existsSync(d)) return [];
|
|
@@ -179,7 +186,7 @@ export class KernelClient {
|
|
|
179
186
|
static async start(pythonPath: string, opts: KernelOptions = {}): Promise<KernelClient> {
|
|
180
187
|
const connPath = join(tmpdir(), `pi-repl-kernel-${randomUUID()}.json`);
|
|
181
188
|
const child = spawn(pythonPath, ["-m", "ipykernel", "-f", connPath, "--no-stdout"], {
|
|
182
|
-
cwd: opts.cwd,
|
|
189
|
+
cwd: resolveCwd(opts.cwd),
|
|
183
190
|
env: { ...process.env, ...(opts.env ?? {}) },
|
|
184
191
|
stdio: ["ignore", "pipe", "pipe"],
|
|
185
192
|
});
|
package/src/extension/prompt.ts
CHANGED
|
@@ -6,81 +6,58 @@
|
|
|
6
6
|
// more signal; the machine reads every line every turn.
|
|
7
7
|
|
|
8
8
|
export const executeToolDescription =
|
|
9
|
-
"You have one tool: a
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"captured separately.";
|
|
9
|
+
"You have one tool: a real `ipython` kernel that stays alive across cells and turns. " +
|
|
10
|
+
"This persistent Python workspace is your only surface — it does the work of bash, read, write, edit, " +
|
|
11
|
+
"search, and file handling, and everything you define (variables, imports, helpers loaded from " +
|
|
12
|
+
"`~/.pi/agent/pi-repl/helpers/`) survives for reuse in later cells. A cell returns its final expression; " +
|
|
13
|
+
"printed output is captured separately.";
|
|
14
14
|
|
|
15
15
|
export const executePromptSnippet =
|
|
16
|
-
"
|
|
17
|
-
"the next step needs, and revise from what you observe.";
|
|
16
|
+
"The persistent Python workspace is your only tool: keep artifacts in variables across cells for reuse, use the loaded helpers, prefer surgical reads/edits over full-file dumps and rewrites, and parse before you print so context stays lean.";
|
|
18
17
|
|
|
19
18
|
// --- the workspace doctrine riding the execute tool ---
|
|
20
19
|
export function buildPromptGuidelines(preloaded: string[]): string[] {
|
|
21
20
|
return [
|
|
22
21
|
"## Your only workspace",
|
|
23
|
-
"`execute` is the only callable tool. Python replaces a read, shell, search, and edit tool rack. State "
|
|
24
|
-
"persists across cells and turns.",
|
|
22
|
+
"`execute` is the only callable tool. Python replaces a read, shell, search, and edit tool rack. State persists across cells and turns.",
|
|
25
23
|
"",
|
|
26
24
|
"## Work in the workspace, not the transcript",
|
|
27
|
-
"Load files, command results,
|
|
28
|
-
"branch, edit, and verify them in later cells. Do not re-read or paste raw material back. Print only the " +
|
|
29
|
-
"small observation needed for the next decision; keep the full artifact in a variable.",
|
|
25
|
+
"Load files, command results, searches, and computed artifacts into variables once; filter, compare, branch, edit, and verify them in later cells. Do not re-read or paste raw material back. Print only the small observation you'll decide on next; keep the full artifact in a variable. A bare final expression is auto-displayed by IPython — assign instead and print only what the next step needs.",
|
|
30
26
|
"",
|
|
31
27
|
"## A cell is a small program",
|
|
32
|
-
"Compose filesystem access, shell commands, searches, transforms, checks, and edits in ordinary Python "
|
|
33
|
-
"when they belong to the same step.",
|
|
28
|
+
"Compose filesystem access, shell commands, searches, transforms, checks, and edits in ordinary Python in the same step.",
|
|
34
29
|
"",
|
|
35
30
|
"## Revise on observations",
|
|
36
31
|
"Revise prior actions or emit new actions upon new observations.", // CodeAct core
|
|
37
32
|
"",
|
|
38
33
|
"## Probe, then build",
|
|
39
|
-
"Inspect what is present — count, print a few lines, list what is loaded — before committing
|
|
40
|
-
"step, run it, and use its output to choose the next.",
|
|
34
|
+
"Inspect what is present — count, print a few lines, list what is loaded — before committing; build one step, run it, and use its output to choose the next.",
|
|
41
35
|
"",
|
|
42
|
-
"##
|
|
43
|
-
"
|
|
44
|
-
"When walking directories, prune generated and hidden dirs — node_modules, .git, .venv, dist, __pycache__ — " +
|
|
45
|
-
"in the walk filter; never print a raw tree. " +
|
|
46
|
-
"For existing files, prefer a surgical old-text/new-text replacement over rewriting the file. Read the " +
|
|
47
|
-
"target region first, make the smallest unique replacement, then verify the changed region and file validity. " +
|
|
48
|
-
"Use complete writes only for new files or intentional full rewrites. Never leave a bare final expression: " +
|
|
49
|
-
"IPython displays it automatically; assign results and explicitly print only what you need.",
|
|
36
|
+
"## File and search work",
|
|
37
|
+
"Prefer a surgical old-text/new-text replacement over rewriting a file: read the region first, make the smallest unique replacement, verify the change and file validity. Use complete writes only for new files or intentional full rewrites. When walking directories, prune generated dirs — node_modules, .git, .venv, dist, __pycache__ — and never print a raw tree.",
|
|
50
38
|
"",
|
|
51
39
|
"## Repository discipline",
|
|
52
|
-
"
|
|
53
|
-
"verify it afterward, and never invent files, APIs, conventions, or test results.",
|
|
40
|
+
"Make the smallest valid change, preserve conventions, verify afterward, and never invent files, APIs, conventions, or test results.",
|
|
54
41
|
"",
|
|
55
|
-
"##
|
|
56
|
-
"Every printed value enters the conversation and
|
|
57
|
-
"raw, recursive, or unbounded results. Explore and filter in variables first, then print only the small, " +
|
|
58
|
-
"bounded observation needed to make the next decision. Never dump an artifact and rely on truncation to " +
|
|
59
|
-
"control it. Quality of output is paramount.",
|
|
60
|
-
"Batch as much independent work as reasonably possible into one call. Keep large values in variables; " +
|
|
61
|
-
"print slices, counts, and summaries.",
|
|
42
|
+
"## Context is expensive",
|
|
43
|
+
"Every printed value enters the conversation. Explore and filter in variables; print only the small, bounded slice for the next decision. Never dump a whole file, a raw result list, or an unbounded output, and never rely on truncation to control it.",
|
|
62
44
|
"",
|
|
63
45
|
...(preloaded.length
|
|
64
46
|
? [
|
|
65
47
|
"## Helpers",
|
|
66
|
-
"
|
|
67
|
-
"appear below.",
|
|
48
|
+
"These helpers are given to you by the user to use directly (loaded from `~/.pi/agent/pi-repl/helpers/`). Descriptions appear below.",
|
|
68
49
|
"",
|
|
69
50
|
...preloaded,
|
|
70
51
|
"",
|
|
71
52
|
]
|
|
72
53
|
: []),
|
|
73
54
|
"## Shell and search",
|
|
74
|
-
"`subprocess.run(
|
|
75
|
-
"kill a silent cell. Use `rg`/`grep`/`find` via `subprocess.run` for deep searches, not Python loops.",
|
|
55
|
+
"Always pass a `timeout` to `subprocess.run(...)` — a silent cell must die, not hang. Use `rg`/`grep`/`find` via the subprocess for deep searches, not Python loops.",
|
|
76
56
|
"",
|
|
77
|
-
"## Environment
|
|
78
|
-
"The evaluator runs in a project-local venv, not the system Python. Do not install a
|
|
79
|
-
"dependencies into the evaluator. Run external projects through their own interface and normal commands.",
|
|
57
|
+
"## Environment & rescue",
|
|
58
|
+
"The evaluator runs in a project-local venv, not the system Python. Do not install a project's dependencies into the evaluator; run external projects through their own interface. If output begins with `<repl_engine_reset>`, the kernel was rebuilt — re-verify any revived variable before reusing it.",
|
|
80
59
|
"",
|
|
81
|
-
"##
|
|
82
|
-
"
|
|
83
|
-
"revived variable before reusing it — especially in a shell command. Functions, classes, and live handles " +
|
|
84
|
-
"are not snapshotted and must be redefined.",
|
|
60
|
+
"## Follow these as the operating manual",
|
|
61
|
+
"These guidelines are how this workspace works — internalize their intent and adapt to this environment by applying it to decisions they do not spell out. Follow them diligently.",
|
|
85
62
|
];
|
|
86
63
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { RestoreResult } from "../engine/index.js";
|
|
4
4
|
|
|
5
5
|
/** Show enough names to orient, then count the rest (a revive can carry hundreds). */
|
|
6
|
-
|
|
6
|
+
function summarizeNames(names: readonly string[], limit: number): string {
|
|
7
7
|
if (names.length <= limit) return names.join(", ");
|
|
8
8
|
return `${names.slice(0, limit).join(", ")} … and ${names.length - limit} more`;
|
|
9
9
|
}
|