pi-repl-py 0.4.0 → 0.6.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/index.ts CHANGED
@@ -151,6 +151,9 @@ export default function (pi: ExtensionAPI) {
151
151
  throw new Error("pi-repl is dormant in this session. Start pi with --repl (or PI_REPL_FORCE=1) to use execute.");
152
152
  }
153
153
  if (ctx?.cwd) location = { cwd: ctx.cwd, sessionFile: ctx.sessionManager?.getSessionFile?.() ?? undefined };
154
+ // --- establish the body slot at call time so Ctrl+O can expand a live (still-awaiting) stream;
155
+ // --- without this the host only renders the result once the first partial or the final result lands ---
156
+ onUpdate?.({ content: [], details: {} });
154
157
  // --- previous engine died mid-session; acquire revives it ---
155
158
  const { engine: m } = await lifecycle.acquire("cell");
156
159
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repl-py",
3
- "version": "0.4.0",
3
+ "version": "0.6.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": [
@@ -116,6 +116,8 @@ export class EngineManager {
116
116
  private startPromise?: Promise<void>;
117
117
  private executionQueue: Promise<unknown> = Promise.resolve();
118
118
  private snapshotTimer?: ReturnType<typeof setTimeout>;
119
+ /** Last-seen top-level namespace names; snapshots are gated on this set changing. */
120
+ private lastNamespaceNames?: string[];
119
121
  private pythonPath?: string;
120
122
 
121
123
  constructor(options: EngineOptions = {}) {
@@ -242,7 +244,7 @@ export class EngineManager {
242
244
  onStream: opts.onStream,
243
245
  maxOutputChars: maxChars,
244
246
  });
245
- if (r.status === "ok") this.scheduleSnapshot();
247
+ if (r.status === "ok") void this.scheduleSnapshotIfChanged();
246
248
  const status: ExecuteResult["status"] = opts.signal?.aborted ? "aborted" : r.status;
247
249
  // Channel cap (truncateWithMarker), then per-line cap; both append a marker so truncation is explicit.
248
250
  const finalize = (text: string, channelTruncated: boolean): string => {
@@ -313,6 +315,20 @@ export class EngineManager {
313
315
  }
314
316
  }
315
317
 
318
+ /** Snapshot only if the set of top-level names changed since the last snapshot. Names-only
319
+ * comparison is cheap (no pickling); a cell that reuses existing state skips the heavy dump. */
320
+ private async scheduleSnapshotIfChanged(): Promise<void> {
321
+ const config = this.options.snapshot;
322
+ if (!config) return;
323
+ const names = await this.listNamespaceNames();
324
+ if (names === null || names.length === 0) return;
325
+ const key = [...names].sort().join(",");
326
+ const prev = this.lastNamespaceNames ? [...this.lastNamespaceNames].sort().join(",") : undefined;
327
+ if (prev !== undefined && prev === key) return; // nothing changed
328
+ this.lastNamespaceNames = [...names].sort();
329
+ this.scheduleSnapshot();
330
+ }
331
+
316
332
  private scheduleSnapshot(): void {
317
333
  const config = this.options.snapshot;
318
334
  if (!config) return;
@@ -6,14 +6,9 @@
6
6
  // more signal; the machine reads every line every turn.
7
7
 
8
8
  export const executeToolDescription =
9
- "Execute Python cells in a persistent ipython kernel that stays alive across cells and turns. " +
10
- "It replaces the default read, bash, edit, write, and search tools file work, shell commands, and " +
11
- "searches all run as Python. Everything you define (variables, imports, and helpers preloaded into the " +
12
- "workspace namespace) survives for reuse in later cells. A cell returns its final expression; printed " +
13
- "output is captured separately. Oversized output is truncated: 1,000,000 characters per cell, 4,096 per " +
14
- "line. Reads are expensive — every printed value enters the context, so hold artifacts in variables, " +
15
- "parse before printing, and print only the small bounded slice the next decision needs. Keep cells lean; " +
16
- "full-file dumps and raw result lists bloat the conversation.";
9
+ "Execute Python cells in a persistent ipython kernel; state survives across cells and turns, replacing " +
10
+ "the default read, bash, edit, write, and search tools. Let a cell's returned value prove the work, " +
11
+ "not prose restating it.";
17
12
 
18
13
  export const executePromptSnippet =
19
14
  "Execute Python cells in a persistent ipython kernel (replaces read, bash, edit, write, and search; state survives across cells and turns)";
@@ -22,28 +17,36 @@ export const executePromptSnippet =
22
17
  export function buildPromptGuidelines(preloaded: string[]): string[] {
23
18
  return [
24
19
  "## Your only workspace",
25
- "`execute` is the only callable tool. Python replaces a read, shell, search, and edit tool rack. State persists across cells and turns.",
20
+ "You are an engineer in a persistent Python REPL. `execute` is the only callable surface it replaces read, bash, edit, write, and search. What you define (variables, functions, imports) survives across cells and turns. The work is proven by the result each cell returns, and by nothing else.",
26
21
  "",
27
- "## Work in the workspace, not the transcript",
28
- "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.",
22
+ "## Get up to speed first",
23
+ "Orient before you act: `%pwd`, glance at the namespace, read any state or progress file, skim recent history. A few tokens, it buys a right first move. Work from what you confirmed, not assumptions.",
29
24
  "",
30
- "## A cell is a small program",
31
- "Compose filesystem access, shell commands, searches, transforms, checks, and edits in ordinary Python in the same step.",
25
+ "## Reason, then say, then stop",
26
+ "Reason as much as the task needs, but reason inside the cell and keep the reasoning out of the transcript: do it in variables and filters, then return only the outcome. Every printed value is expensive — it enters the context now and stays, costing later tokens every time — so print only what the next decision consumes. A bare final expression auto-prints, so assign instead. Concise reasoning still works — length you cut is reward you don't lose, because the evidence is the returned result, not the words around it.",
32
27
  "",
33
- "## Revise on observations",
34
- "Revise prior actions or emit new actions upon new observations.", // CodeAct core
28
+ "## The environment answers you",
29
+ "The cell's output is the ground truth what actually ran, what errored, what came back. Trust it over any narrative: if a cell already proved it, point at that. When you're unsure what a fetch contains, read a slice, don't guess and don't dump it whole to 'check'.",
35
30
  "",
36
- "## Probe, then build",
37
- "Inspect what is presentcount, print a few lines, list what is loaded before committing; build one step, run it, and use its output to choose the next.",
31
+ "## Gather, slice, decide",
32
+ "Fetch into a variable, never into the transcript. Search results, reads, command output, file contents assign. A bare expression prints, so end those cells on the assignment. Then advance on a bounded slice: print only the fragment that decides the next step, hold the rest in the variable, peel into the pieces you need without re-fetching, and when the reasoning lands, print the conclusion.",
38
33
  "",
39
- "## File and search work",
40
- "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.",
34
+ "Reading whole is fine when the task needs all of it — hold it and reason on it; the point isn't to never read fully, it's to not re-fetch the same big thing twice.",
41
35
  "",
42
- "## Repository discipline",
43
- "Make the smallest valid change, preserve conventions, verify afterward, and never invent files, APIs, conventions, or test results.",
36
+ "## Output format",
37
+ "In reply text: the conclusion and the handful of results that prove it — the slice you acted on, the returned value, a one-line takeaway. Do not transcribe the run, restate every variable, or narrate what the cell already showed.",
44
38
  "",
45
- "## Context is expensive",
46
- "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.",
39
+ "## Worked example",
40
+ "Gather and slice two cells, thin transcript:\n cell 1: doc = open('notes.txt').read()\n cell 2: print(doc.splitlines()[:5])\nThe whole file lands in doc (nothing printed); the second cell prints only the first five lines, the rest stays in doc for later.",
41
+ "",
42
+ "## Compose and reuse",
43
+ "Compose filesystem, shell, search, transforms, checks, edits in ordinary Python in one cell, and end on the value the next step consumes. A step seen twice becomes a function you call once — proven work, reused. Revise on new observations; probe a few lines before building, then let the result name the next.",
44
+ "",
45
+ "## Edits and repo discipline",
46
+ "Surgical old-text/new-text: read the region, fix an exact unique anchor that appears once, replace, verify. Many small edits over one big rewrite — a parse error can strand an anchor; after an error, read the file back from disk first. Make the smallest valid change, preserve conventions, never invent files, APIs, conventions, or test results. Prune generated dirs when walking trees.",
47
+ "",
48
+ "## Shell & search",
49
+ "Always pass a `timeout` to `subprocess.run(...)` — a silent cell must die, not hang. Capture output in a variable and read a slice, not the whole stdout. Use `rg`/`grep`/`find` for deep searches, not Python loops.",
47
50
  "",
48
51
  ...(preloaded.length
49
52
  ? [
@@ -54,13 +57,10 @@ export function buildPromptGuidelines(preloaded: string[]): string[] {
54
57
  "",
55
58
  ]
56
59
  : []),
57
- "## Shell and search",
58
- "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.",
59
- "",
60
60
  "## Environment & rescue",
61
- "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.",
61
+ "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 rebuilt — re-verify a revived variable before reusing it.",
62
62
  "",
63
- "## Follow these as the operating manual",
64
- "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.",
63
+ "## When a rule doesn't cover it",
64
+ "If something isn't spelled out, keep working rather than asking: hold it in the workspace, prove it with a returned result, and keep the transcript to what you act on. Make the sensible default and correct it from the result.",
65
65
  ];
66
66
  }