pi-repl-py 0.3.0 → 0.5.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
@@ -4,6 +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 { withSkillsBlock } from "./src/extension/skill-hook.js";
7
8
  import { EngineManager } from "./src/engine/index.js";
8
9
  import { ExecuteCellComponent, type ExecuteDetails, type ExecuteRenderState } from "./src/extension/render.js";
9
10
  import { EngineLifecycle } from "./src/extension/session-engine.js";
@@ -109,6 +110,16 @@ export default function (pi: ExtensionAPI) {
109
110
  return { content: event.content, details: stashed.details, isError: true };
110
111
  });
111
112
 
113
+ // --- pi gates skills on the read tool (absent in repl); re-emit them via withSkillsBlock. ---
114
+ pi.on("before_agent_start", (event) => {
115
+ if (!active()) return;
116
+ const systemPrompt = withSkillsBlock(
117
+ event.systemPrompt,
118
+ event.systemPromptOptions?.skills ?? [],
119
+ );
120
+ return systemPrompt === undefined ? undefined : { systemPrompt };
121
+ });
122
+
112
123
  pi.registerTool<typeof executeSchema, ExecuteDetails, Partial<ExecuteRenderState>>({
113
124
  name: "execute",
114
125
  label: "execute",
@@ -140,6 +151,9 @@ export default function (pi: ExtensionAPI) {
140
151
  throw new Error("pi-repl is dormant in this session. Start pi with --repl (or PI_REPL_FORCE=1) to use execute.");
141
152
  }
142
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: {} });
143
157
  // --- previous engine died mid-session; acquire revives it ---
144
158
  const { engine: m } = await lifecycle.acquire("cell");
145
159
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-repl-py",
3
- "version": "0.3.0",
3
+ "version": "0.5.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": [
@@ -6,58 +6,71 @@
6
6
  // more signal; the machine reads every line every turn.
7
7
 
8
8
  export const executeToolDescription =
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.";
9
+ "Execute Python cells in a persistent ipython kernel that stays alive across cells and turns, replacing " +
10
+ "the default read, bash, edit, write, and search tools. Everything you define (variables, imports, and " +
11
+ "helpers preloaded into the workspace namespace) survives for reuse in later cells. A cell returns its " +
12
+ "final expression bare final expressions are auto-printed, and output is trimmed at 1,000,000 " +
13
+ "characters per cell / 4,096 per line. Treat these as facts about how the workspace reports, not as " +
14
+ "limits to test: assign the values you want to keep so they stay in scope for later cells, and let a " +
15
+ "cell's return value be the proof of its work rather than re-stating that work in prose.";
14
16
 
15
17
  export const executePromptSnippet =
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
+ "Execute Python cells in a persistent ipython kernel (replaces read, bash, edit, write, and search; state survives across cells and turns)";
17
19
 
18
20
  // --- the workspace doctrine riding the execute tool ---
19
21
  export function buildPromptGuidelines(preloaded: string[]): string[] {
20
22
  return [
21
23
  "## Your only workspace",
22
- "`execute` is the only callable tool. Python replaces a read, shell, search, and edit tool rack. State persists across cells and turns.",
24
+ "`execute` is your workspace: a persistent Python session that is the only callable surface. What you define — variables, functions, data — survives across cells and turns, and the work is proven by the results each cell returns.",
23
25
  "",
24
- "## Work in the workspace, not the transcript",
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.",
26
+ "## Go deep in the cell; prove it by the result",
27
+ "Depth, the cell, not the transcript. Do the heavy reasoning in variables and filters; a cell's worth is shown by what it returns as a result, not by restating that result in prose. Every printed value enters the context and equating length with value is the trap so let the code's result, not a recap paragraph, be the evidence.",
28
+ "",
29
+ "## The gather-filter-advance shape",
30
+ "Leave raw data in the workspace. Search results, reads, command output, file contents — whatever you fetch — land in variables, never in the transcript.",
31
+ "",
32
+ "1. **Gather.** One cell assigns the whole: result = search(q), doc = load(path), out = run(...) — nothing printed, ends on the assignment.",
33
+ "2. **Advance.** The next cell prints only the fragment that decides the next step — titles only, a slice of content — and you pick from that sliver.",
34
+ "3. **Peel, don't re-fetch.** You already hold the whole; walk into the pieces you need without re-running it.",
35
+ "4. **Emit, then drop.** When the reasoning lands, print the conclusion; the rest stays in the variable, or is overwritten when done.",
36
+ "",
37
+ "Each printed value is the one that changes the next cell; the transcript stays thin, the work dense in variables.",
38
+ "",
39
+ "Windows, not bans: reading something whole is fine when the task genuinely needs all of it — do that, then keep reasoning on it. The point is not to never read fully; it is to read by window by default and hold the whole, so you never re-fetch the same big thing twice.",
26
40
  "",
27
41
  "## A cell is a small program",
28
- "Compose filesystem access, shell commands, searches, transforms, checks, and edits in ordinary Python in the same step.",
42
+ "Compose whatever the step needs — filesystem, shell, search, transforms — in one cell, and end it on the return value the next step consumes. The cell itself (what ran) carries the meaning; the transcript carries only that returned value.",
43
+ "",
44
+ "Name what recurs: when the same operation shows up twice, give it a name once — a function in the namespace — and call it. A defined function is work already proven; every call is a new return-value result, and you never re-print the steps that made it.",
29
45
  "",
30
46
  "## Revise on observations",
31
47
  "Revise prior actions or emit new actions upon new observations.", // CodeAct core
32
48
  "",
33
49
  "## Probe, then build",
34
- "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.",
50
+ "Inspect where you are — a small slice — before committing; then build one step and let its returned result name the next. The proof of each step is the cell's result, not a summary of it.",
35
51
  "",
36
52
  "## 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.",
53
+ "Prefer a surgical old-text/new-text replacement over rewriting a file: read the region first, fix an exact unique anchor that appears once, replace exactly, then verify. Prefer many small verified edits over one big blind rewrite — a parse error mid-way can strand an anchor. Use complete writes only for new files or intentional full rewrites. After an edit errors or writes a partial result, read the file back from disk before reasoning about it. When walking directories, prune generated dirs and never print a raw tree.",
38
54
  "",
39
55
  "## Repository discipline",
40
56
  "Make the smallest valid change, preserve conventions, verify afterward, and never invent files, APIs, conventions, or test results.",
41
57
  "",
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.",
44
- "",
45
58
  ...(preloaded.length
46
59
  ? [
47
60
  "## Helpers",
48
- "These helpers are given to you by the user to use directly (loaded from `~/.pi/agent/pi-repl/helpers/`). Descriptions appear below.",
61
+ "These helpers are already defined in the workspace namespace. Use them by name as you would any other loaded function, class, or variable. Their code already executed at kernel boot. Descriptions appear below.",
49
62
  "",
50
63
  ...preloaded,
51
64
  "",
52
65
  ]
53
66
  : []),
54
67
  "## Shell and search",
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.",
68
+ "Always pass a `timeout` to `subprocess.run(...)` — a silent cell must die, not hang. Capture the result in a variable and read a slice, not dump the whole stdout into the transcript: use `rg`/`grep`/`find` for deep searches, not Python loops.",
56
69
  "",
57
70
  "## Environment & rescue",
58
71
  "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.",
59
72
  "",
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.",
73
+ "## The operating principle above the manual",
74
+ "The rules above are working forms of one principle: the work happens in the workspace — in cells and their results and the transcript carries only what decides or concludes. When a case isn't spelled out, apply the principle over the example: wherever the work can live in the workspace instead of the transcript, keep it there, and let the returned result be the proof. The result is the certificate; the rest of the work stays out of the reply.",
62
75
  ];
63
76
  }
@@ -0,0 +1,22 @@
1
+ // --- skills cannot reach the prompt in --repl: pi gates <available_skills> on the read tool
2
+ // (hasRead), which repl doesn't have. Re-emit them with pi's own formatter in pi's slot. ---
3
+ import { formatSkillsForPrompt, type Skill } from "@mariozechner/pi-coding-agent";
4
+
5
+ const CWD_MARKER = "\nCurrent working directory:"; // skills sit just before this, pi's last line
6
+ const READ_LINE = "Use the read tool to load a skill's file when the task matches its description."; // canon line
7
+ const EXECUTE_LINE = "Load a skill's SKILL.md file contents via execute (read the file with Python)."; // repl has no read
8
+
9
+ /** The prompt with the skills block in pi's slot; undefined if nothing should change. */
10
+ export function withSkillsBlock(
11
+ prompt: string,
12
+ skills: Skill[],
13
+ alreadyPresent = prompt.includes("<available_skills>"),
14
+ ): string | undefined {
15
+ if (skills.length === 0) return undefined;
16
+ let extra = formatSkillsForPrompt(skills);
17
+ if (!extra) return undefined;
18
+ if (alreadyPresent) return undefined;
19
+ extra = extra.replace(READ_LINE, EXECUTE_LINE);
20
+ const idx = prompt.indexOf(CWD_MARKER);
21
+ return idx === -1 ? prompt + extra : prompt.slice(0, idx) + extra + prompt.slice(idx);
22
+ }