pi-repl-py 0.2.6 → 0.2.8

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
@@ -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
@@ -87,10 +87,12 @@ finished draining on iopub. A cell settles only when **both** the `execute_reply
87
87
  matching iopub `status idle` (published after every byte of output) have arrived. Settling
88
88
  on the reply alone would drop output that was still in flight.
89
89
 
90
- **Output is capped per channel.** Each channel accumulates output against a character budget
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.
90
+ **Output is capped per channel and per line.** Each channel accumulates output against a character
91
+ budget (`maxOutputChars`), checked within each message, so overflow trips the moment a message
92
+ exceeds the budget rather than when it churns on. Each individual line is also capped at a generous
93
+ length (4096 chars), so a single genuinely oversized line cannot own the whole budget while
94
+ legitimately long REPL output (JSON, reprs, errors) still passes through whole. Both truncations
95
+ are announced with explicit markers so the model knows output was cut.
94
96
 
95
97
  **Cancellation is real.** An abort sends an `interrupt_request` on the control channel,
96
98
  which raises a genuine `KeyboardInterrupt` in the running cell; the namespace survives. As a
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repl-py",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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": [
@@ -26,6 +26,10 @@ function resolvePythonPath(cwd: string | undefined): string {
26
26
  }
27
27
 
28
28
  const DEFAULT_MAX_OUTPUT_CHARS = 65536;
29
+ /** Per-line cap: one genuinely oversized line must not own the channel budget, while legitimately long
30
+ * REPL output (JSON, reprs, errors) still fits under the cap in one piece. Generous enough that only
31
+ * pathological giant lines are trimmed, unlike pi's grep where the line cap keeps matches terse. */
32
+ export const MAX_OUTPUT_LINE_CHARS = 4096;
29
33
  const ABORT_GRACE_MS = 500;
30
34
  const DEFAULT_SNAPSHOT_DEBOUNCE_MS = 1500;
31
35
 
@@ -98,6 +102,18 @@ function truncateWithMarker(text: string, maxChars: number, wasTruncated: boolea
98
102
  return `${text.slice(0, maxChars)}\n[... output truncated at ${maxChars} chars ...]`;
99
103
  }
100
104
 
105
+ /** Cap each individual line, so one giant line cannot own the whole channel budget (like grep's line cap). */
106
+ export function capLinesForContext(text: string): { text: string; trimmed: boolean } {
107
+ const lines = text.split("\n");
108
+ let trimmed = false;
109
+ const mapped = lines.map((line) => {
110
+ if (line.length <= MAX_OUTPUT_LINE_CHARS) return line;
111
+ trimmed = true;
112
+ return line.slice(0, MAX_OUTPUT_LINE_CHARS);
113
+ });
114
+ return { text: mapped.join("\n"), trimmed };
115
+ }
116
+
101
117
  export class EngineManager {
102
118
  private readonly options: EngineOptions;
103
119
  private kernel?: KernelClient;
@@ -233,11 +249,18 @@ export class EngineManager {
233
249
  });
234
250
  if (r.status === "ok") this.scheduleSnapshot();
235
251
  const status: ExecuteResult["status"] = opts.signal?.aborted ? "aborted" : r.status;
236
- const truncate = (text: string, truncated: boolean) => truncateWithMarker(text, maxChars, truncated);
252
+ // Channel cap (truncateWithMarker), then per-line cap; both append a marker so truncation is explicit.
253
+ const finalize = (text: string, channelTruncated: boolean): string => {
254
+ let out = truncateWithMarker(text, maxChars, channelTruncated);
255
+ const line = capLinesForContext(out);
256
+ if (line.trimmed) out = `${line.text}\n[... some lines exceeded ${MAX_OUTPUT_LINE_CHARS} chars ...]`;
257
+ else out = line.text;
258
+ return out;
259
+ };
237
260
  return {
238
- stdout: truncate(r.stdout, r.truncated?.stdout ?? false),
239
- stderr: truncate(r.stderr, r.truncated?.stderr ?? false),
240
- result: r.result !== undefined ? truncate(String(r.result), String(r.result).length > maxChars) : undefined,
261
+ stdout: finalize(r.stdout, r.truncated?.stdout ?? false),
262
+ stderr: finalize(r.stderr, r.truncated?.stderr ?? false),
263
+ result: r.result !== undefined ? finalize(String(r.result), String(r.result).length > maxChars) : undefined,
241
264
  error: r.error,
242
265
  status,
243
266
  durationMs: Date.now() - started,
@@ -6,75 +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 persistent Python workspace backed by a real `ipython` kernel. " +
10
- "Variables, imports, and definitions survive across cells and turns — it is your working memory and action " +
11
- "language. " +
12
- "Helpers in `~/.pi/agent/pi-repl/helpers/` load at boot. A cell returns its final expression; printed output is " +
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
- "Work in the workspace: keep artifacts in variables, compose related actions in Python, print only what " +
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, search hits, and computed artifacts into variables once; filter, compare, " +
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. Build one " +
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
- "## 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.",
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.",
48
38
  "",
49
39
  "## 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.",
40
+ "Make the smallest valid change, preserve conventions, verify afterward, and never invent files, APIs, conventions, or test results.",
52
41
  "",
53
- "## Batch and print sparingly",
54
- "Batch as much independent work as reasonably possible into one call. Keep large values in variables; " +
55
- "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.",
56
44
  "",
57
45
  ...(preloaded.length
58
46
  ? [
59
47
  "## Helpers",
60
- "User helpers load from `~/.pi/agent/pi-repl/helpers/` as workspace definitions. Their descriptions " +
61
- "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.",
62
49
  "",
63
50
  ...preloaded,
64
51
  "",
65
52
  ]
66
53
  : []),
67
54
  "## Shell and search",
68
- "`subprocess.run(..., timeout=...)` when you need a result always set a `timeout`, the evaluator does not " +
69
- "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.",
70
56
  "",
71
- "## Environment boundary",
72
- "The evaluator runs in a project-local venv, not the system Python. Do not install a target project's " +
73
- "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.",
74
59
  "",
75
- "## Engine reset guard",
76
- "If output begins with `<repl_engine_reset>`, the kernel was rebuilt from the last snapshot. Re-verify a " +
77
- "revived variable before reusing it — especially in a shell command. Functions, classes, and live handles " +
78
- "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.",
79
62
  ];
80
63
  }