@oh-my-pi/pi-coding-agent 14.5.11 → 14.5.13

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +18 -10
  3. package/src/cli/jupyter-cli.ts +1 -1
  4. package/src/config/model-equivalence.ts +49 -16
  5. package/src/config/model-registry.ts +100 -25
  6. package/src/config/model-resolver.ts +29 -15
  7. package/src/config/settings-schema.ts +20 -6
  8. package/src/config/settings.ts +9 -8
  9. package/src/config.ts +9 -0
  10. package/src/eval/backend.ts +43 -0
  11. package/src/eval/eval.lark +43 -0
  12. package/src/eval/index.ts +5 -0
  13. package/src/eval/js/context-manager.ts +717 -0
  14. package/src/eval/js/executor.ts +131 -0
  15. package/src/eval/js/index.ts +46 -0
  16. package/src/eval/js/prelude.ts +2 -0
  17. package/src/eval/js/prelude.txt +84 -0
  18. package/src/eval/js/tool-bridge.ts +124 -0
  19. package/src/eval/parse.ts +337 -0
  20. package/src/{ipy → eval/py}/executor.ts +2 -180
  21. package/src/{ipy → eval/py}/gateway-coordinator.ts +4 -3
  22. package/src/eval/py/index.ts +58 -0
  23. package/src/{ipy → eval/py}/kernel.ts +5 -41
  24. package/src/{ipy → eval/py}/prelude.py +39 -227
  25. package/src/eval/types.ts +48 -0
  26. package/src/export/html/template.generated.ts +1 -1
  27. package/src/export/html/template.js +23 -17
  28. package/src/extensibility/extensions/types.ts +2 -3
  29. package/src/internal-urls/docs-index.generated.ts +5 -5
  30. package/src/lsp/client.ts +9 -0
  31. package/src/lsp/index.ts +395 -0
  32. package/src/lsp/types.ts +15 -4
  33. package/src/main.ts +25 -14
  34. package/src/mcp/oauth-flow.ts +1 -1
  35. package/src/memories/index.ts +1 -1
  36. package/src/modes/acp/acp-event-mapper.ts +1 -1
  37. package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
  38. package/src/modes/components/login-dialog.ts +1 -1
  39. package/src/modes/components/oauth-selector.ts +2 -1
  40. package/src/modes/components/tool-execution.ts +3 -4
  41. package/src/modes/controllers/command-controller.ts +28 -8
  42. package/src/modes/controllers/input-controller.ts +4 -4
  43. package/src/modes/controllers/selector-controller.ts +2 -1
  44. package/src/modes/interactive-mode.ts +4 -5
  45. package/src/modes/types.ts +3 -3
  46. package/src/modes/utils/ui-helpers.ts +2 -2
  47. package/src/prompts/system/system-prompt.md +3 -3
  48. package/src/prompts/tools/atom.md +3 -2
  49. package/src/prompts/tools/browser.md +61 -16
  50. package/src/prompts/tools/eval.md +92 -0
  51. package/src/prompts/tools/lsp.md +7 -3
  52. package/src/sdk.ts +45 -31
  53. package/src/session/agent-session.ts +44 -54
  54. package/src/session/messages.ts +1 -1
  55. package/src/slash-commands/builtin-registry.ts +1 -1
  56. package/src/system-prompt.ts +34 -66
  57. package/src/task/executor.ts +5 -9
  58. package/src/tools/browser/attach.ts +175 -0
  59. package/src/tools/browser/launch.ts +576 -0
  60. package/src/tools/browser/readable.ts +90 -0
  61. package/src/tools/browser/registry.ts +198 -0
  62. package/src/tools/browser/render.ts +212 -0
  63. package/src/tools/browser/tab-protocol.ts +101 -0
  64. package/src/tools/browser/tab-supervisor.ts +429 -0
  65. package/src/tools/browser/tab-worker-entry.ts +21 -0
  66. package/src/tools/browser/tab-worker.ts +1006 -0
  67. package/src/tools/browser.ts +231 -1567
  68. package/src/tools/checkpoint.ts +2 -2
  69. package/src/tools/{python.ts → eval.ts} +324 -315
  70. package/src/tools/exit-plan-mode.ts +1 -1
  71. package/src/tools/index.ts +62 -100
  72. package/src/tools/plan-mode-guard.ts +27 -1
  73. package/src/tools/read.ts +0 -6
  74. package/src/tools/recipe/runners/pkg.ts +34 -32
  75. package/src/tools/renderers.ts +4 -2
  76. package/src/tools/resolve.ts +7 -2
  77. package/src/tools/todo-write.ts +0 -1
  78. package/src/tools/tool-timeouts.ts +2 -2
  79. package/src/utils/markit.ts +15 -7
  80. package/src/utils/tools-manager.ts +5 -5
  81. package/src/web/search/index.ts +5 -5
  82. package/src/web/search/provider.ts +121 -39
  83. package/src/web/search/providers/gemini.ts +2 -2
  84. package/src/web/search/render.ts +2 -2
  85. package/src/ipy/modules.ts +0 -144
  86. package/src/prompts/tools/python.md +0 -57
  87. /package/src/{ipy → eval/py}/cancellation.ts +0 -0
  88. /package/src/{ipy → eval/py}/prelude.ts +0 -0
  89. /package/src/{ipy → eval/py}/runtime.ts +0 -0
@@ -0,0 +1,337 @@
1
+ import type { EvalLanguage } from "./types";
2
+
3
+ export type EvalLanguageOrigin = "default" | "fence";
4
+
5
+ export interface ParsedEvalCell {
6
+ index: number;
7
+ title?: string;
8
+ code: string;
9
+ language: EvalLanguage;
10
+ languageOrigin: EvalLanguageOrigin;
11
+ timeoutMs: number;
12
+ reset: boolean;
13
+ }
14
+
15
+ export interface ParsedEvalInput {
16
+ cells: ParsedEvalCell[];
17
+ }
18
+
19
+ const DEFAULT_TIMEOUT_MS = 30_000;
20
+
21
+ /**
22
+ * Canonical fenced-language tokens we map onto our two backends. Matched
23
+ * case-insensitively. Anything else found in a fence info string is treated as
24
+ * a title fragment rather than a language; this is intentional fallback
25
+ * behaviour and MUST NOT be advertised in the tool's prompt — the lark grammar
26
+ * describes the canonical surface we encourage callers to emit.
27
+ */
28
+ const LANGUAGE_ALIASES: Record<string, EvalLanguage> = {
29
+ py: "python",
30
+ python: "python",
31
+ ipy: "python",
32
+ ipython: "python",
33
+ js: "js",
34
+ javascript: "js",
35
+ ts: "js",
36
+ typescript: "js",
37
+ };
38
+
39
+ function resolveLanguageAlias(token: string): EvalLanguage | undefined {
40
+ return LANGUAGE_ALIASES[token.toLowerCase()];
41
+ }
42
+
43
+ /**
44
+ * Map an attribute key (from `key=value` in a fence info string) to one of
45
+ * the three canonical roles. Canonical keys: `id`, `t`, `rst`. Fallback
46
+ * aliases — accepted but not advertised in the prompt — cover common
47
+ * synonyms the LLM is likely to reach for instead of the short canonical.
48
+ */
49
+ const ID_KEYS = new Set(["id", "title", "name", "cell", "file", "label"]);
50
+ const T_KEYS = new Set(["t", "timeout", "duration", "time"]);
51
+ const RST_KEYS = new Set(["rst", "reset"]);
52
+
53
+ function classifyAttrKey(key: string): "id" | "t" | "rst" | null {
54
+ if (ID_KEYS.has(key)) return "id";
55
+ if (T_KEYS.has(key)) return "t";
56
+ if (RST_KEYS.has(key)) return "rst";
57
+ return null;
58
+ }
59
+
60
+ interface RawBlock {
61
+ type: "raw";
62
+ lines: string[];
63
+ startLine: number;
64
+ }
65
+
66
+ interface FencedBlock {
67
+ type: "fenced";
68
+ info: string;
69
+ codeLines: string[];
70
+ startLine: number;
71
+ }
72
+
73
+ type Block = RawBlock | FencedBlock;
74
+
75
+ interface FenceInfo {
76
+ language?: EvalLanguage;
77
+ title?: string;
78
+ timeoutMs?: number;
79
+ reset?: boolean;
80
+ }
81
+
82
+ const ATTR_TOKEN_RE = /^([a-zA-Z][\w-]*)=(?:"([^"]*)"|'([^']*)'|(.*))$/;
83
+ const DURATION_TOKEN_RE = /^\d+(?:ms|s|m)?$/;
84
+
85
+ function parseDurationMs(raw: string, lineNumber: number): number {
86
+ const match = /^(\d+)(ms|s|m)?$/.exec(raw.trim());
87
+ if (!match) {
88
+ throw new Error(
89
+ `Eval line ${lineNumber}: invalid duration \`${raw}\`; use a number with optional ms, s, or m units.`,
90
+ );
91
+ }
92
+ const value = Number.parseInt(match[1], 10);
93
+ const unit = match[2] ?? "s";
94
+ if (unit === "ms") return value;
95
+ if (unit === "s") return value * 1000;
96
+ return value * 60_000;
97
+ }
98
+
99
+ function parseBoolean(value: string): boolean | undefined {
100
+ const normalized = value.trim().toLowerCase();
101
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") return true;
102
+ if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") return false;
103
+ return undefined;
104
+ }
105
+
106
+ function trimOuterBlankLines(lines: string[]): string[] {
107
+ let start = 0;
108
+ let end = lines.length;
109
+ while (start < end && lines[start].trim() === "") start++;
110
+ while (end > start && lines[end - 1].trim() === "") end--;
111
+ return lines.slice(start, end);
112
+ }
113
+
114
+ function parseFenceOpener(line: string): { char: "`" | "~"; count: number; info: string } | null {
115
+ const opener = /^(`{3,}|~{3,})(.*)$/.exec(line);
116
+ if (!opener) return null;
117
+ const run = opener[1];
118
+ return { char: run[0] as "`" | "~", count: run.length, info: opener[2].trim() };
119
+ }
120
+
121
+ function isFenceCloser(line: string, char: "`" | "~", minCount: number): boolean {
122
+ let count = 0;
123
+ while (count < line.length && line[count] === char) count++;
124
+ if (count < minCount) return false;
125
+ return line.slice(count).trim() === "";
126
+ }
127
+
128
+ /**
129
+ * Tokenize a fence info string while preserving content inside matching
130
+ * single or double quotes as a single token. The opening and closing quote
131
+ * characters are kept verbatim so attribute parsing can strip them later.
132
+ */
133
+ function tokenizeInfoString(info: string): string[] {
134
+ const tokens: string[] = [];
135
+ let i = 0;
136
+ while (i < info.length) {
137
+ while (i < info.length && /\s/.test(info[i])) i++;
138
+ if (i >= info.length) break;
139
+ let token = "";
140
+ while (i < info.length && !/\s/.test(info[i])) {
141
+ const ch = info[i];
142
+ if (ch === '"' || ch === "'") {
143
+ token += ch;
144
+ i++;
145
+ while (i < info.length && info[i] !== ch) {
146
+ token += info[i];
147
+ i++;
148
+ }
149
+ if (i < info.length) {
150
+ token += info[i];
151
+ i++;
152
+ }
153
+ } else {
154
+ token += ch;
155
+ i++;
156
+ }
157
+ }
158
+ tokens.push(token);
159
+ }
160
+ return tokens;
161
+ }
162
+
163
+ /**
164
+ * Decode a fence info string into language, title, timeout, and reset flag.
165
+ *
166
+ * Layout (positional → kv, all optional):
167
+ * `<lang>? <duration>? <(title-fragment | key=value)>*`
168
+ *
169
+ * Canonical attribute keys (the only ones surfaced in the lark grammar):
170
+ * - `id` → cell title
171
+ * - `t` → per-cell timeout
172
+ * - `rst` → boolean reset for this cell's kernel
173
+ *
174
+ * Lenient fallback aliases (NOT advertised in the prompt; we silently accept
175
+ * them when the LLM reaches for a more familiar key):
176
+ * - id: title, name, cell, file, label
177
+ * - t: timeout, duration, time
178
+ * - rst: reset
179
+ *
180
+ * Truly unknown keys are silently dropped. First occurrence wins when a key
181
+ * is repeated (canonical or alias).
182
+ *
183
+ * - First token is consumed as a language alias when it matches one; otherwise
184
+ * it falls through to the title-fragment branch and the cell inherits the
185
+ * surrounding language.
186
+ * - The first remaining duration-shaped token (e.g. `15s`, `500ms`, `2m`,
187
+ * `30`) becomes the positional timeout. The `t=` attribute always wins.
188
+ * - Anything else accumulates as positional title fragments joined by spaces.
189
+ */
190
+ function parseFenceInfo(info: string, lineNumber: number): FenceInfo {
191
+ const tokens = tokenizeInfoString(info.trim());
192
+ if (tokens.length === 0) return {};
193
+
194
+ let language: EvalLanguage | undefined;
195
+ let positionalDurationMs: number | undefined;
196
+ const titleParts: string[] = [];
197
+ let idAttr: string | undefined;
198
+ let tAttr: string | undefined;
199
+ let rstAttr: string | undefined;
200
+
201
+ for (let idx = 0; idx < tokens.length; idx++) {
202
+ const token = tokens[idx];
203
+ const attrMatch = ATTR_TOKEN_RE.exec(token);
204
+ if (attrMatch) {
205
+ const key = attrMatch[1].toLowerCase();
206
+ const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
207
+ const role = classifyAttrKey(key);
208
+ if (role === "id" && idAttr === undefined) idAttr = value;
209
+ else if (role === "t" && tAttr === undefined) tAttr = value;
210
+ else if (role === "rst" && rstAttr === undefined) rstAttr = value;
211
+ // unknown / repeated keys silently dropped
212
+ continue;
213
+ }
214
+ if (idx === 0) {
215
+ const lang = resolveLanguageAlias(token);
216
+ if (lang) {
217
+ language = lang;
218
+ continue;
219
+ }
220
+ }
221
+ if (positionalDurationMs === undefined && DURATION_TOKEN_RE.test(token)) {
222
+ positionalDurationMs = parseDurationMs(token, lineNumber);
223
+ continue;
224
+ }
225
+ titleParts.push(token);
226
+ }
227
+
228
+ const explicitTitle = (idAttr ?? "").trim();
229
+ const positionalTitle = titleParts.join(" ").trim();
230
+ const title = explicitTitle.length > 0 ? explicitTitle : positionalTitle.length > 0 ? positionalTitle : undefined;
231
+
232
+ let timeoutMs: number | undefined;
233
+ if (tAttr !== undefined) {
234
+ timeoutMs = parseDurationMs(tAttr, lineNumber);
235
+ } else if (positionalDurationMs !== undefined) {
236
+ timeoutMs = positionalDurationMs;
237
+ }
238
+
239
+ let reset: boolean | undefined;
240
+ if (rstAttr !== undefined) {
241
+ const parsed = parseBoolean(rstAttr);
242
+ if (parsed === undefined) {
243
+ throw new Error(`Eval line ${lineNumber}: invalid rst value \`${rstAttr}\`; use true or false.`);
244
+ }
245
+ reset = parsed;
246
+ }
247
+
248
+ return { language, title, timeoutMs, reset };
249
+ }
250
+
251
+ /**
252
+ * Walk normalized lines and split into top-level fenced blocks and raw
253
+ * (between/around fences) blocks. Unclosed fences are leniently closed at
254
+ * end-of-input. Raw blocks with only blank lines are dropped.
255
+ */
256
+ function splitIntoBlocks(lines: string[]): Block[] {
257
+ const blocks: Block[] = [];
258
+ let i = 0;
259
+ while (i < lines.length) {
260
+ const line = lines[i];
261
+ const opener = parseFenceOpener(line);
262
+ if (opener) {
263
+ const fenceStart = i + 1; // 1-indexed line number of opener
264
+ const codeLines: string[] = [];
265
+ let j = i + 1;
266
+ let closed = false;
267
+ while (j < lines.length) {
268
+ if (isFenceCloser(lines[j], opener.char, opener.count)) {
269
+ closed = true;
270
+ break;
271
+ }
272
+ codeLines.push(lines[j]);
273
+ j++;
274
+ }
275
+ blocks.push({ type: "fenced", info: opener.info, codeLines, startLine: fenceStart });
276
+ i = closed ? j + 1 : j;
277
+ } else {
278
+ const rawStart = i + 1;
279
+ const rawLines: string[] = [line];
280
+ let j = i + 1;
281
+ while (j < lines.length && !parseFenceOpener(lines[j])) {
282
+ rawLines.push(lines[j]);
283
+ j++;
284
+ }
285
+ const trimmed = trimOuterBlankLines(rawLines);
286
+ if (trimmed.length > 0) {
287
+ blocks.push({ type: "raw", lines: trimmed, startLine: rawStart });
288
+ }
289
+ i = j;
290
+ }
291
+ }
292
+ return blocks;
293
+ }
294
+
295
+ interface ExpansionState {
296
+ language: EvalLanguage;
297
+ languageOrigin: EvalLanguageOrigin;
298
+ }
299
+
300
+ export function parseEvalInput(input: string): ParsedEvalInput {
301
+ const normalized = input.replace(/\r\n?/g, "\n");
302
+ const lines = normalized.split("\n");
303
+ const blocks = splitIntoBlocks(lines);
304
+
305
+ const state: ExpansionState = { language: "python", languageOrigin: "default" };
306
+ const cells: ParsedEvalCell[] = [];
307
+ for (const block of blocks) {
308
+ if (block.type === "raw") {
309
+ cells.push({
310
+ index: cells.length,
311
+ title: undefined,
312
+ code: block.lines.join("\n"),
313
+ language: state.language,
314
+ languageOrigin: state.languageOrigin,
315
+ timeoutMs: DEFAULT_TIMEOUT_MS,
316
+ reset: false,
317
+ });
318
+ continue;
319
+ }
320
+ const fence = parseFenceInfo(block.info, block.startLine);
321
+ const language = fence.language ?? state.language;
322
+ const languageOrigin: EvalLanguageOrigin = fence.language ? "fence" : state.languageOrigin;
323
+ cells.push({
324
+ index: cells.length,
325
+ title: fence.title,
326
+ code: block.codeLines.join("\n"),
327
+ language,
328
+ languageOrigin,
329
+ timeoutMs: fence.timeoutMs ?? DEFAULT_TIMEOUT_MS,
330
+ reset: fence.reset ?? false,
331
+ });
332
+ state.language = language;
333
+ state.languageOrigin = languageOrigin;
334
+ }
335
+
336
+ return { cells };
337
+ }
@@ -1,17 +1,13 @@
1
- import * as path from "node:path";
2
- import { getAgentDir, getProjectDir, isBunTestRuntime, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
- import { OutputSink } from "../session/streaming-output";
1
+ import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
2
+ import { OutputSink } from "../../session/streaming-output";
4
3
  import { shutdownSharedGateway } from "./gateway-coordinator";
5
4
  import {
6
5
  checkPythonKernelAvailability,
7
6
  type KernelDisplayOutput,
8
7
  type KernelExecuteOptions,
9
8
  type KernelExecuteResult,
10
- type PreludeHelper,
11
9
  PythonKernel,
12
10
  } from "./kernel";
13
- import { discoverPythonModules } from "./modules";
14
- import { PYTHON_PRELUDE } from "./prelude";
15
11
 
16
12
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
17
13
  const MAX_KERNEL_SESSIONS = 4;
@@ -101,7 +97,6 @@ interface KernelSession {
101
97
 
102
98
  const kernelSessions = new Map<string, KernelSession>();
103
99
  const disposingKernelSessions = new Set<KernelSession>();
104
- let cachedPreludeDocs: PreludeHelper[] | null = null;
105
100
  let cleanupTimer: NodeJS.Timeout | null = null;
106
101
 
107
102
  interface KernelSessionExecutionOptions {
@@ -256,110 +251,6 @@ function buildKernelStartOptions(
256
251
  };
257
252
  }
258
253
 
259
- interface PreludeCacheSource {
260
- path: string;
261
- hash: string;
262
- }
263
-
264
- interface PreludeCachePayload {
265
- helpers: PreludeHelper[];
266
- sources: PreludeCacheSource[];
267
- }
268
-
269
- interface PreludeCacheState {
270
- cacheKey: string;
271
- cachePath: string;
272
- sources: PreludeCacheSource[];
273
- }
274
-
275
- const PRELUDE_CACHE_DIR = "pycache";
276
-
277
- function hashPreludeContent(content: string): string {
278
- return Bun.hash(content).toString(16);
279
- }
280
-
281
- async function buildPreludeCacheState(cwd: string): Promise<PreludeCacheState> {
282
- const modules = await discoverPythonModules({ cwd });
283
- const moduleSources = modules
284
- .map(module => ({ path: module.path, hash: hashPreludeContent(module.content) }))
285
- .sort((a, b) => a.path.localeCompare(b.path));
286
- const sources: PreludeCacheSource[] = [
287
- { path: "omp:prelude", hash: hashPreludeContent(PYTHON_PRELUDE) },
288
- ...moduleSources,
289
- ];
290
- const composite = sources.map(source => `${source.path}:${source.hash}`).join("|");
291
- const cacheKey = Bun.hash(composite).toString(16);
292
- const cachePath = path.join(getAgentDir(), PRELUDE_CACHE_DIR, `${cacheKey}.json`);
293
- return { cacheKey, cachePath, sources };
294
- }
295
-
296
- async function readPreludeCache(state: PreludeCacheState): Promise<PreludeHelper[] | null> {
297
- let raw: string;
298
- try {
299
- raw = await Bun.file(state.cachePath).text();
300
- } catch (err) {
301
- if (isEnoent(err)) return null;
302
- logger.warn("Failed to read Python prelude cache", { path: state.cachePath, error: String(err) });
303
- return null;
304
- }
305
- try {
306
- const parsed = JSON.parse(raw) as PreludeCachePayload | PreludeHelper[];
307
- const helpers = Array.isArray(parsed) ? parsed : parsed.helpers;
308
- if (!Array.isArray(helpers) || helpers.length === 0) return null;
309
- return helpers;
310
- } catch (err) {
311
- logger.warn("Failed to parse Python prelude cache", { path: state.cachePath, error: String(err) });
312
- return null;
313
- }
314
- }
315
-
316
- async function writePreludeCache(state: PreludeCacheState, helpers: PreludeHelper[]): Promise<void> {
317
- const payload: PreludeCachePayload = { helpers, sources: state.sources };
318
- try {
319
- await Bun.write(state.cachePath, JSON.stringify(payload));
320
- } catch (err) {
321
- logger.warn("Failed to write Python prelude cache", { path: state.cachePath, error: String(err) });
322
- }
323
- }
324
-
325
- function getPreludeIntrospectionOptions(
326
- options: KernelSessionExecutionOptions = {},
327
- ): Pick<KernelExecuteOptions, "signal" | "timeoutMs"> {
328
- return {
329
- signal: options.signal,
330
- timeoutMs: requireRemainingTimeoutMs(options.deadlineMs),
331
- };
332
- }
333
-
334
- async function cachePreludeDocs(
335
- cwd: string,
336
- docs: PreludeHelper[],
337
- cacheState?: PreludeCacheState | null,
338
- ): Promise<PreludeHelper[]> {
339
- cachedPreludeDocs = docs;
340
- if (!isBunTestRuntime() && docs.length > 0) {
341
- const state = cacheState ?? (await buildPreludeCacheState(cwd));
342
- await writePreludeCache(state, docs);
343
- }
344
- return docs;
345
- }
346
-
347
- async function ensurePreludeDocsLoaded(
348
- kernel: PythonKernel,
349
- cwd: string,
350
- options: KernelSessionExecutionOptions = {},
351
- cacheState?: PreludeCacheState | null,
352
- ): Promise<PreludeHelper[]> {
353
- if (cachedPreludeDocs && cachedPreludeDocs.length > 0) {
354
- return cachedPreludeDocs;
355
- }
356
- const docs = await kernel.introspectPrelude(getPreludeIntrospectionOptions(options));
357
- if (docs.length === 0) {
358
- throw new Error("Python prelude helpers unavailable");
359
- }
360
- return cachePreludeDocs(cwd, docs, cacheState);
361
- }
362
-
363
254
  function startCleanupTimer(): void {
364
255
  if (cleanupTimer) return;
365
256
  cleanupTimer = setInterval(() => {
@@ -562,75 +453,6 @@ async function ensureKernelAvailable(
562
453
  }
563
454
  }
564
455
 
565
- export async function warmPythonEnvironment(
566
- cwd: string,
567
- sessionId?: string,
568
- useSharedGateway?: boolean,
569
- sessionFile?: string,
570
- kernelOwnerId?: string,
571
- signal?: AbortSignal,
572
- ): Promise<{ ok: boolean; reason?: string; docs: PreludeHelper[] }> {
573
- let cacheState: PreludeCacheState | null = null;
574
- const resolvedSessionId = sessionId ?? `session:${cwd}`;
575
- try {
576
- await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd, { signal });
577
- } catch (err: unknown) {
578
- const reason = err instanceof Error ? err.message : String(err);
579
- cachedPreludeDocs = [];
580
- return { ok: false, reason, docs: [] };
581
- }
582
- if (!isBunTestRuntime()) {
583
- try {
584
- cacheState = await buildPreludeCacheState(cwd);
585
- const cached = await readPreludeCache(cacheState);
586
- if (cached) {
587
- cachedPreludeDocs = cached;
588
- attachKernelOwner(resolvedSessionId, kernelOwnerId);
589
- return { ok: true, docs: cached };
590
- }
591
- } catch (err) {
592
- logger.warn("Failed to resolve Python prelude cache", { error: String(err) });
593
- cacheState = null;
594
- }
595
- }
596
- if (cachedPreludeDocs && cachedPreludeDocs.length > 0) {
597
- attachKernelOwner(resolvedSessionId, kernelOwnerId);
598
- return { ok: true, docs: cachedPreludeDocs };
599
- }
600
- try {
601
- const docs = await logger.time(
602
- "warmPython:withKernelSession",
603
- withKernelSession,
604
- resolvedSessionId,
605
- cwd,
606
- kernel => ensurePreludeDocsLoaded(kernel, cwd, { useSharedGateway, sessionFile, signal }, cacheState),
607
- {
608
- useSharedGateway,
609
- sessionFile,
610
- kernelOwnerId,
611
- signal,
612
- },
613
- );
614
- return { ok: true, docs };
615
- } catch (err: unknown) {
616
- const reason = err instanceof Error ? err.message : String(err);
617
- cachedPreludeDocs = [];
618
- return { ok: false, reason, docs: [] };
619
- }
620
- }
621
-
622
- export function getPreludeDocs(): PreludeHelper[] {
623
- return cachedPreludeDocs ?? [];
624
- }
625
-
626
- export function setPreludeDocsCache(docs: PreludeHelper[]): void {
627
- cachedPreludeDocs = docs;
628
- }
629
-
630
- export function resetPreludeDocsCache(): void {
631
- cachedPreludeDocs = null;
632
- }
633
-
634
456
  function isResourceExhaustionError(error: unknown): boolean {
635
457
  const message = error instanceof Error ? error.message : String(error);
636
458
  return (
@@ -1,10 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import { createServer } from "node:net";
3
3
  import * as path from "node:path";
4
+ import { Process } from "@oh-my-pi/pi-natives";
4
5
  import { getAgentDir, isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
5
6
  import type { Subprocess } from "bun";
6
- import { Settings } from "../config/settings";
7
- import { getOrCreateSnapshot } from "../utils/shell-snapshot";
7
+ import { Settings } from "../../config/settings";
8
+ import { getOrCreateSnapshot } from "../../utils/shell-snapshot";
8
9
  import { filterEnv, resolvePythonRuntime } from "./runtime";
9
10
 
10
11
  const GATEWAY_DIR_NAME = "python-gateway";
@@ -300,7 +301,7 @@ async function startGatewayProcess(
300
301
 
301
302
  async function killGateway(pid: number, context: string): Promise<void> {
302
303
  try {
303
- await procmgr.terminate({ target: pid });
304
+ await Process.fromPid(pid)?.terminate();
304
305
  } catch (err) {
305
306
  logger.warn("Failed to kill shared gateway process", {
306
307
  error: err instanceof Error ? err.message : String(err),
@@ -0,0 +1,58 @@
1
+ import type { ToolSession } from "../../tools";
2
+ import type { ExecutorBackend, ExecutorBackendExecOptions, ExecutorBackendResult } from "../backend";
3
+ import { executePython, type PythonExecutorOptions } from "./executor";
4
+ import { checkPythonKernelAvailability } from "./kernel";
5
+
6
+ const PYTHON_SESSION_PREFIX = "python:";
7
+
8
+ function namespaceSessionId(sessionId: string): string {
9
+ return sessionId.startsWith(PYTHON_SESSION_PREFIX) ? sessionId : `${PYTHON_SESSION_PREFIX}${sessionId}`;
10
+ }
11
+
12
+ function readSetting<T>(session: ToolSession, key: string): T | undefined {
13
+ const settings = session.settings as { get?: (key: string) => T | undefined } | undefined;
14
+ return settings?.get?.(key);
15
+ }
16
+
17
+ export default {
18
+ id: "python",
19
+ label: "Python",
20
+ highlightLang: "python",
21
+
22
+ async isAvailable(session: ToolSession): Promise<boolean> {
23
+ const availability = await checkPythonKernelAvailability(session.cwd);
24
+ return availability.ok;
25
+ },
26
+
27
+ async execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult> {
28
+ const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
29
+ const kernelMode = readSetting<PythonExecutorOptions["kernelMode"]>(opts.session, "python.kernelMode");
30
+ const executorOptions: PythonExecutorOptions = {
31
+ cwd: opts.cwd,
32
+ deadlineMs: opts.deadlineMs,
33
+ signal: opts.signal,
34
+ sessionId: namespaceSessionId(opts.sessionId),
35
+ kernelMode,
36
+ useSharedGateway,
37
+ sessionFile: opts.sessionFile,
38
+ kernelOwnerId: opts.kernelOwnerId,
39
+ reset: opts.reset,
40
+ artifactPath: opts.artifactPath,
41
+ artifactId: opts.artifactId,
42
+ onChunk: opts.onChunk,
43
+ };
44
+ const result = await executePython(code, executorOptions);
45
+ return {
46
+ output: result.output,
47
+ exitCode: result.exitCode,
48
+ cancelled: result.cancelled,
49
+ truncated: result.truncated,
50
+ artifactId: result.artifactId,
51
+ totalLines: result.totalLines,
52
+ totalBytes: result.totalBytes,
53
+ outputLines: result.outputLines,
54
+ outputBytes: result.outputBytes,
55
+ displayOutputs: result.displayOutputs,
56
+ };
57
+ },
58
+ } satisfies ExecutorBackend;