@oh-my-pi/pi-coding-agent 14.5.12 → 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 (85) hide show
  1. package/CHANGELOG.md +32 -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 +2 -2
  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 +8 -10
  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/eval.md +92 -0
  49. package/src/prompts/tools/lsp.md +7 -3
  50. package/src/sdk.ts +45 -31
  51. package/src/session/agent-session.ts +42 -42
  52. package/src/session/messages.ts +1 -1
  53. package/src/slash-commands/builtin-registry.ts +1 -1
  54. package/src/system-prompt.ts +34 -66
  55. package/src/task/executor.ts +5 -9
  56. package/src/tools/browser/launch.ts +22 -0
  57. package/src/tools/browser/registry.ts +25 -244
  58. package/src/tools/browser/render.ts +1 -1
  59. package/src/tools/browser/tab-protocol.ts +101 -0
  60. package/src/tools/browser/tab-supervisor.ts +429 -0
  61. package/src/tools/browser/tab-worker-entry.ts +21 -0
  62. package/src/tools/browser/tab-worker.ts +1006 -0
  63. package/src/tools/browser.ts +12 -29
  64. package/src/tools/checkpoint.ts +2 -2
  65. package/src/tools/{python.ts → eval.ts} +324 -315
  66. package/src/tools/exit-plan-mode.ts +1 -1
  67. package/src/tools/index.ts +62 -100
  68. package/src/tools/read.ts +0 -6
  69. package/src/tools/recipe/runners/pkg.ts +34 -32
  70. package/src/tools/renderers.ts +2 -2
  71. package/src/tools/resolve.ts +7 -2
  72. package/src/tools/todo-write.ts +0 -1
  73. package/src/tools/tool-timeouts.ts +2 -2
  74. package/src/utils/markit.ts +15 -7
  75. package/src/utils/tools-manager.ts +5 -5
  76. package/src/web/search/index.ts +5 -5
  77. package/src/web/search/provider.ts +121 -39
  78. package/src/web/search/providers/gemini.ts +2 -2
  79. package/src/web/search/render.ts +2 -2
  80. package/src/ipy/modules.ts +0 -144
  81. package/src/prompts/tools/python.md +0 -57
  82. package/src/tools/browser/vm.ts +0 -792
  83. /package/src/{ipy → eval/py}/cancellation.ts +0 -0
  84. /package/src/{ipy → eval/py}/prelude.ts +0 -0
  85. /package/src/{ipy → eval/py}/runtime.ts +0 -0
@@ -0,0 +1,429 @@
1
+ import { getPuppeteerDir, logger, Snowflake } from "@oh-my-pi/pi-utils";
2
+ import type { Page, Target } from "puppeteer-core";
3
+ import type { ToolSession } from "../../sdk";
4
+ import { expandPath } from "../path-utils";
5
+ import { ToolAbortError, ToolError } from "../tool-errors";
6
+ import { pickElectronTarget } from "./attach";
7
+ import { type BrowserHandle, type BrowserKindTag, holdBrowser, releaseBrowser } from "./registry";
8
+ import type {
9
+ ReadyInfo,
10
+ RunErrorPayload,
11
+ RunResultOk,
12
+ SessionSnapshot,
13
+ Transferable,
14
+ Transport,
15
+ WorkerInbound,
16
+ WorkerInitPayload,
17
+ WorkerOutbound,
18
+ } from "./tab-protocol";
19
+ import { WorkerCore } from "./tab-worker";
20
+
21
+ interface WorkerHandle {
22
+ send(msg: WorkerInbound, transferList?: Transferable[]): void;
23
+ onMessage(handler: (msg: WorkerOutbound) => void): () => void;
24
+ terminate(): Promise<void>;
25
+ readonly mode: "worker" | "inline";
26
+ }
27
+
28
+ export type DialogPolicy = "accept" | "dismiss";
29
+
30
+ export interface TabSession {
31
+ name: string;
32
+ browser: BrowserHandle;
33
+ targetId: string;
34
+ worker: WorkerHandle;
35
+ state: "alive" | "dead";
36
+ info: ReadyInfo;
37
+ pending: Map<string, { resolve: (result: RunResultOk) => void; reject: (error: unknown) => void }>;
38
+ dialogPolicy?: DialogPolicy;
39
+ kindTag: BrowserKindTag;
40
+ }
41
+
42
+ export interface AcquireTabOptions {
43
+ url?: string;
44
+ waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
45
+ viewport?: { width: number; height: number; deviceScaleFactor?: number };
46
+ target?: string;
47
+ signal?: AbortSignal;
48
+ timeoutMs: number;
49
+ dialogs?: DialogPolicy;
50
+ }
51
+
52
+ export interface AcquireTabResult {
53
+ tab: TabSession;
54
+ created: boolean;
55
+ }
56
+
57
+ export interface RunInTabOptions {
58
+ code: string;
59
+ timeoutMs: number;
60
+ signal?: AbortSignal;
61
+ session: ToolSession;
62
+ }
63
+
64
+ export interface ReleaseTabOptions {
65
+ kill?: boolean;
66
+ }
67
+
68
+ const tabs = new Map<string, TabSession>();
69
+ const GRACE_MS = 750;
70
+
71
+ export function getTab(name: string): TabSession | undefined {
72
+ return tabs.get(name);
73
+ }
74
+
75
+ export function listTabs(): TabSession[] {
76
+ return [...tabs.values()];
77
+ }
78
+
79
+ export async function acquireTab(
80
+ name: string,
81
+ browser: BrowserHandle,
82
+ opts: AcquireTabOptions,
83
+ ): Promise<AcquireTabResult> {
84
+ const existing = tabs.get(name);
85
+ if (existing) {
86
+ if (existing.browser === browser && existing.state === "alive") {
87
+ if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
88
+ await releaseTab(name, { kill: false });
89
+ } else {
90
+ if (opts.url) {
91
+ await runInTabWithSnapshot(
92
+ name,
93
+ {
94
+ code: `await tab.goto(${JSON.stringify(opts.url)}, { waitUntil: ${JSON.stringify(opts.waitUntil ?? "networkidle2")} });`,
95
+ timeoutMs: opts.timeoutMs,
96
+ signal: opts.signal,
97
+ },
98
+ { cwd: process.cwd() },
99
+ );
100
+ }
101
+ return { tab: tabs.get(name)!, created: false };
102
+ }
103
+ } else {
104
+ await releaseTab(name, { kill: false });
105
+ }
106
+ }
107
+
108
+ const initPayload = await buildInitPayload(browser, opts);
109
+ const worker = await spawnTabWorker();
110
+ const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
111
+ const unlisten = worker.onMessage(msg => {
112
+ if (msg.type === "ready") resolve(msg.info);
113
+ else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
114
+ else if (msg.type === "log") logWorkerMessage(msg);
115
+ });
116
+ let info: ReadyInfo;
117
+ try {
118
+ worker.send({ type: "init", payload: initPayload });
119
+ info = await raceWithTimeout(promise, opts.timeoutMs + GRACE_MS, "Timed out initializing browser tab worker");
120
+ } catch (error) {
121
+ unlisten();
122
+ await worker.terminate().catch(() => undefined);
123
+ if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
124
+ throw error;
125
+ }
126
+ unlisten();
127
+
128
+ holdBrowser(browser);
129
+ const tab: TabSession = {
130
+ name,
131
+ browser,
132
+ targetId: info.targetId,
133
+ worker,
134
+ state: "alive",
135
+ info,
136
+ pending: new Map(),
137
+ dialogPolicy: opts.dialogs,
138
+ kindTag: browser.kind.kind,
139
+ };
140
+ worker.onMessage(msg => handleTabMessage(tab, msg));
141
+ tabs.set(name, tab);
142
+ return { tab, created: true };
143
+ }
144
+
145
+ export async function runInTab(name: string, opts: RunInTabOptions): Promise<RunResultOk> {
146
+ return await runInTabWithSnapshot(
147
+ name,
148
+ { code: opts.code, timeoutMs: opts.timeoutMs, signal: opts.signal },
149
+ { cwd: opts.session.cwd, browserScreenshotDir: expandBrowserScreenshotDir(opts.session) },
150
+ );
151
+ }
152
+
153
+ async function runInTabWithSnapshot(
154
+ name: string,
155
+ opts: { code: string; timeoutMs: number; signal?: AbortSignal },
156
+ snapshot: SessionSnapshot,
157
+ ): Promise<RunResultOk> {
158
+ const tab = tabs.get(name);
159
+ if (!tab || tab.state === "dead") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
160
+ if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
161
+ const id = Snowflake.next();
162
+ const { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();
163
+ tab.pending.set(id, { resolve, reject });
164
+ const abort = (): void => tab.worker.send({ type: "abort", id });
165
+ if (opts.signal?.aborted) abort();
166
+ else opts.signal?.addEventListener("abort", abort, { once: true });
167
+ try {
168
+ tab.worker.send({ type: "run", id, name, code: opts.code, timeoutMs: opts.timeoutMs, session: snapshot });
169
+ return await raceWithTimeout(
170
+ promise,
171
+ opts.timeoutMs + GRACE_MS,
172
+ "Browser code execution hung past grace; tab killed",
173
+ async reason => await forceKillTab(name, reason),
174
+ );
175
+ } finally {
176
+ opts.signal?.removeEventListener("abort", abort);
177
+ tab.pending.delete(id);
178
+ }
179
+ }
180
+
181
+ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Promise<boolean> {
182
+ const tab = tabs.get(name);
183
+ if (!tab) {
184
+ logger.debug("releaseTab: unknown tab", { name });
185
+ return false;
186
+ }
187
+ const wasAlive = tab.state === "alive";
188
+ tab.state = "dead";
189
+ const closeError = new ToolError(`Tab ${JSON.stringify(name)} was closed`);
190
+ for (const [id, pending] of tab.pending) {
191
+ try {
192
+ tab.worker.send({ type: "abort", id });
193
+ } catch {}
194
+ pending.reject(closeError);
195
+ }
196
+ tab.pending.clear();
197
+ let forced = false;
198
+ if (wasAlive) {
199
+ try {
200
+ tab.worker.send({ type: "close" });
201
+ await waitForClosed(tab);
202
+ } catch {
203
+ forced = true;
204
+ }
205
+ }
206
+ await tab.worker.terminate().catch(() => undefined);
207
+ if (forced && tab.kindTag === "headless") await closeOrphanTarget(tab);
208
+ await releaseBrowser(tab.browser, { kill: opts.kill ?? false });
209
+ tabs.delete(name);
210
+ return true;
211
+ }
212
+
213
+ export async function releaseAllTabs(opts: ReleaseTabOptions = {}): Promise<number> {
214
+ const names = [...tabs.keys()];
215
+ let count = 0;
216
+ for (const name of names) {
217
+ if (await releaseTab(name, opts)) count++;
218
+ }
219
+ return count;
220
+ }
221
+
222
+ export async function dropHeadlessTabs(): Promise<void> {
223
+ const names = [...tabs.values()].filter(tab => tab.kindTag === "headless").map(tab => tab.name);
224
+ for (const name of names) await releaseTab(name);
225
+ }
226
+
227
+ async function buildInitPayload(browser: BrowserHandle, opts: AcquireTabOptions): Promise<WorkerInitPayload> {
228
+ const safeDir = getPuppeteerDir();
229
+ const browserWSEndpoint = browser.browser.wsEndpoint();
230
+ if (!browserWSEndpoint) throw new ToolError("Browser websocket endpoint is unavailable");
231
+ if (browser.kind.kind === "headless") {
232
+ return {
233
+ mode: "headless",
234
+ browserWSEndpoint,
235
+ safeDir,
236
+ viewport: opts.viewport,
237
+ dialogs: opts.dialogs,
238
+ url: opts.url,
239
+ waitUntil: opts.waitUntil,
240
+ timeoutMs: opts.timeoutMs,
241
+ };
242
+ }
243
+ const page = await pickElectronTarget(browser.browser, opts.target);
244
+ const targetId = await targetIdForPage(page);
245
+ return {
246
+ mode: "attach",
247
+ browserWSEndpoint,
248
+ safeDir,
249
+ targetId,
250
+ dialogs: opts.dialogs,
251
+ };
252
+ }
253
+
254
+ function handleTabMessage(tab: TabSession, msg: WorkerOutbound): void {
255
+ if (msg.type === "result") {
256
+ const pending = tab.pending.get(msg.id);
257
+ if (!pending) return;
258
+ tab.pending.delete(msg.id);
259
+ if (msg.ok) {
260
+ pending.resolve(msg.payload);
261
+ return;
262
+ }
263
+ pending.reject(errorFromPayload(msg.error));
264
+ return;
265
+ }
266
+ if (msg.type === "ready") {
267
+ tab.info = msg.info;
268
+ return;
269
+ }
270
+ if (msg.type === "log") logWorkerMessage(msg);
271
+ }
272
+
273
+ async function forceKillTab(name: string, reason: string): Promise<void> {
274
+ const tab = tabs.get(name);
275
+ if (!tab) return;
276
+ tab.state = "dead";
277
+ const error = new ToolError(reason);
278
+ for (const pending of tab.pending.values()) pending.reject(error);
279
+ tab.pending.clear();
280
+ await tab.worker.terminate().catch(() => undefined);
281
+ if (tab.kindTag === "headless") await closeOrphanTarget(tab);
282
+ await releaseBrowser(tab.browser, { kill: false });
283
+ tabs.delete(name);
284
+ }
285
+
286
+ async function closeOrphanTarget(tab: TabSession): Promise<void> {
287
+ for (const target of tab.browser.browser.targets()) {
288
+ if ((await targetIdForTarget(target).catch(() => "")) !== tab.targetId) continue;
289
+ const page = await target.page().catch(() => null);
290
+ await page?.close().catch(() => undefined);
291
+ return;
292
+ }
293
+ }
294
+
295
+ async function waitForClosed(tab: TabSession): Promise<void> {
296
+ const { promise, resolve } = Promise.withResolvers<void>();
297
+ const unsubscribe = tab.worker.onMessage(msg => {
298
+ if (msg.type === "closed") resolve();
299
+ });
300
+ try {
301
+ await raceWithTimeout(promise, GRACE_MS, "Timed out closing browser tab worker");
302
+ } finally {
303
+ unsubscribe();
304
+ }
305
+ }
306
+
307
+ function expandBrowserScreenshotDir(session: ToolSession): string | undefined {
308
+ const value = session.settings.get("browser.screenshotDir") as string | undefined;
309
+ return value ? expandPath(value) : undefined;
310
+ }
311
+
312
+ async function targetIdForPage(page: Page): Promise<string> {
313
+ return await targetIdForTarget(page.target());
314
+ }
315
+
316
+ async function targetIdForTarget(target: Target): Promise<string> {
317
+ const raw = target as unknown as { _targetId?: unknown };
318
+ if (typeof raw._targetId === "string") return raw._targetId;
319
+ const session = await target.createCDPSession();
320
+ try {
321
+ const info = (await session.send("Target.getTargetInfo")) as { targetInfo?: { targetId?: string } };
322
+ if (info.targetInfo?.targetId) return info.targetInfo.targetId;
323
+ throw new ToolError("Target id unavailable from CDP target info");
324
+ } finally {
325
+ await session.detach().catch(() => undefined);
326
+ }
327
+ }
328
+
329
+ function errorFromPayload(payload: RunErrorPayload): Error {
330
+ const error = payload.isAbort
331
+ ? new ToolAbortError()
332
+ : payload.isToolError
333
+ ? new ToolError(payload.message)
334
+ : new Error(payload.message);
335
+ error.name = payload.name;
336
+ if (payload.stack) error.stack = payload.stack;
337
+ return error;
338
+ }
339
+
340
+ function logWorkerMessage(msg: Extract<WorkerOutbound, { type: "log" }>): void {
341
+ if (msg.level === "debug") logger.debug(msg.msg, msg.meta);
342
+ else if (msg.level === "warn") logger.warn(msg.msg, msg.meta);
343
+ else logger.error(msg.msg, msg.meta);
344
+ }
345
+
346
+ async function raceWithTimeout<T>(
347
+ promise: Promise<T>,
348
+ timeoutMs: number,
349
+ reason: string,
350
+ onTimeout?: (reason: string) => Promise<void>,
351
+ ): Promise<T> {
352
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
353
+ const { promise: timeoutPromise, reject } = Promise.withResolvers<never>();
354
+ const onAbort = (): void => reject(new ToolError(reason));
355
+ timeoutSignal.addEventListener("abort", onAbort, { once: true });
356
+ try {
357
+ return await Promise.race([promise, timeoutPromise]);
358
+ } catch (error) {
359
+ if (error instanceof ToolError && error.message === reason) await onTimeout?.(reason);
360
+ throw error;
361
+ } finally {
362
+ timeoutSignal.removeEventListener("abort", onAbort);
363
+ }
364
+ }
365
+
366
+ async function spawnTabWorker(): Promise<WorkerHandle> {
367
+ try {
368
+ const url = new URL("./tab-worker-entry.ts", import.meta.url);
369
+ const worker = new Worker(url.href, { type: "module" });
370
+ return wrapBunWorker(worker);
371
+ } catch (err) {
372
+ logger.warn("Bun Worker spawn failed; using inline tab worker (no sync-loop guard)", {
373
+ error: err instanceof Error ? err.message : String(err),
374
+ });
375
+ return spawnInlineWorker();
376
+ }
377
+ }
378
+
379
+ function wrapBunWorker(worker: Worker): WorkerHandle {
380
+ return {
381
+ mode: "worker",
382
+ send(msg, transferList) {
383
+ worker.postMessage(msg, { transfer: transferList ?? [] });
384
+ },
385
+ onMessage(handler) {
386
+ const wrap = (event: MessageEvent): void => handler(event.data as WorkerOutbound);
387
+ worker.addEventListener("message", wrap);
388
+ return () => worker.removeEventListener("message", wrap);
389
+ },
390
+ async terminate() {
391
+ worker.terminate();
392
+ },
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Inline fallback for environments where Bun cannot compile or spawn the worker
398
+ * entry. This preserves normal browser behavior but cannot interrupt synchronous
399
+ * infinite loops because user code runs on the main thread.
400
+ */
401
+ function spawnInlineWorker(): WorkerHandle {
402
+ const hostListeners = new Set<(message: WorkerOutbound) => void>();
403
+ const workerListeners = new Set<(message: WorkerInbound) => void>();
404
+ const workerTransport: Transport = {
405
+ send: msg =>
406
+ queueMicrotask(() => {
407
+ for (const listener of hostListeners) listener(msg as WorkerOutbound);
408
+ }),
409
+ onMessage: handler => {
410
+ const typed = handler as (message: WorkerInbound) => void;
411
+ workerListeners.add(typed);
412
+ return () => workerListeners.delete(typed);
413
+ },
414
+ close: () => {},
415
+ };
416
+ new WorkerCore(workerTransport);
417
+ return {
418
+ mode: "inline",
419
+ send: msg =>
420
+ queueMicrotask(() => {
421
+ for (const listener of workerListeners) listener(msg);
422
+ }),
423
+ onMessage: handler => {
424
+ hostListeners.add(handler);
425
+ return () => hostListeners.delete(handler);
426
+ },
427
+ async terminate() {},
428
+ };
429
+ }
@@ -0,0 +1,21 @@
1
+ import { parentPort } from "node:worker_threads";
2
+ import type { Transport, WorkerInbound, WorkerOutbound } from "./tab-protocol";
3
+ import { WorkerCore } from "./tab-worker";
4
+
5
+ if (!parentPort) throw new Error("tab-worker-entry: missing parentPort");
6
+
7
+ const transport: Transport = {
8
+ send(msg, transferList) {
9
+ parentPort!.postMessage(msg, transferList ?? []);
10
+ },
11
+ onMessage(handler) {
12
+ const wrap = (message: unknown): void => handler(message as WorkerOutbound | WorkerInbound);
13
+ parentPort!.on("message", wrap);
14
+ return () => parentPort!.off("message", wrap);
15
+ },
16
+ close() {
17
+ parentPort!.close();
18
+ },
19
+ };
20
+
21
+ new WorkerCore(transport);