@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.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.
Files changed (54) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/package.json +7 -7
  3. package/scripts/generate-template.ts +4 -3
  4. package/src/cli/setup-cli.ts +14 -161
  5. package/src/cli/stats-cli.ts +56 -2
  6. package/src/cli.ts +0 -1
  7. package/src/config/settings-schema.ts +0 -10
  8. package/src/eval/eval.lark +30 -10
  9. package/src/eval/js/context-manager.ts +334 -564
  10. package/src/eval/js/shared/helpers.ts +237 -0
  11. package/src/eval/js/shared/indirect-eval.ts +30 -0
  12. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  13. package/src/eval/js/shared/runtime.ts +168 -0
  14. package/src/eval/js/shared/types.ts +18 -0
  15. package/src/eval/js/tool-bridge.ts +2 -4
  16. package/src/eval/js/worker-core.ts +146 -0
  17. package/src/eval/js/worker-entry.ts +24 -0
  18. package/src/eval/js/worker-protocol.ts +41 -0
  19. package/src/eval/parse.ts +218 -49
  20. package/src/eval/py/display.ts +71 -0
  21. package/src/eval/py/executor.ts +74 -89
  22. package/src/eval/py/index.ts +1 -2
  23. package/src/eval/py/kernel.ts +472 -900
  24. package/src/eval/py/prelude.py +95 -7
  25. package/src/eval/py/runner.py +879 -0
  26. package/src/eval/py/runtime.ts +3 -16
  27. package/src/eval/py/tool-bridge.ts +137 -0
  28. package/src/export/html/index.ts +5 -2
  29. package/src/export/html/template.generated.ts +1 -1
  30. package/src/export/html/template.js +93 -5
  31. package/src/export/html/template.macro.ts +4 -3
  32. package/src/internal-urls/docs-index.generated.ts +3 -3
  33. package/src/modes/components/read-tool-group.ts +9 -0
  34. package/src/modes/controllers/command-controller.ts +0 -23
  35. package/src/prompts/tools/eval.md +14 -27
  36. package/src/prompts/tools/read.md +1 -0
  37. package/src/session/agent-session.ts +0 -1
  38. package/src/session/history-storage.ts +77 -19
  39. package/src/tools/browser/tab-protocol.ts +4 -0
  40. package/src/tools/browser/tab-supervisor.ts +86 -5
  41. package/src/tools/browser/tab-worker.ts +104 -58
  42. package/src/tools/conflict-detect.ts +661 -0
  43. package/src/tools/eval.ts +1 -1
  44. package/src/tools/index.ts +6 -0
  45. package/src/tools/path-utils.ts +1 -1
  46. package/src/tools/read.ts +130 -0
  47. package/src/tools/write.ts +204 -0
  48. package/src/web/search/index.ts +6 -4
  49. package/src/cli/jupyter-cli.ts +0 -106
  50. package/src/commands/jupyter.ts +0 -32
  51. package/src/eval/py/cancellation.ts +0 -28
  52. package/src/eval/py/gateway-coordinator.ts +0 -424
  53. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
  54. /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import * as vm from "node:vm";
4
+
5
5
  import { Snowflake, untilAborted } from "@oh-my-pi/pi-utils";
6
6
  import type { HTMLElement } from "linkedom";
7
7
  import type {
@@ -14,6 +14,8 @@ import type {
14
14
  SerializedAXNode,
15
15
  Target,
16
16
  } from "puppeteer-core";
17
+ import { JsRuntime, type RuntimeHooks } from "../../eval/js/shared/runtime";
18
+ import type { JsDisplayOutput } from "../../eval/js/shared/types";
17
19
  import { resizeImage } from "../../utils/image-resize";
18
20
  import { resolveToCwd } from "../path-utils";
19
21
  import { formatScreenshot } from "../render-utils";
@@ -34,6 +36,7 @@ import type {
34
36
  RunResultOk,
35
37
  ScreenshotResult,
36
38
  SessionSnapshot,
39
+ ToolReply,
37
40
  Transport,
38
41
  WorkerInbound,
39
42
  WorkerInitPayload,
@@ -177,6 +180,27 @@ function errorPayload(error: unknown): RunErrorPayload {
177
180
  return { name: "Error", message: String(error), isToolError: false, isAbort: false };
178
181
  }
179
182
 
183
+ function safeJsonStringify(value: unknown): string {
184
+ try {
185
+ return JSON.stringify(value, null, 2);
186
+ } catch {
187
+ return String(value);
188
+ }
189
+ }
190
+
191
+ function replyError(payload: RunErrorPayload): Error {
192
+ if (payload.isAbort) {
193
+ const err = new ToolAbortError(payload.message || "Tool call aborted");
194
+ if (payload.stack) err.stack = payload.stack;
195
+ return err;
196
+ }
197
+ const Ctor = payload.isToolError ? ToolError : Error;
198
+ const err = new Ctor(payload.message);
199
+ if (payload.name) err.name = payload.name;
200
+ if (payload.stack) err.stack = payload.stack;
201
+ return err;
202
+ }
203
+
180
204
  async function targetIdForTarget(target: Target): Promise<string> {
181
205
  const raw = target as unknown as { _targetId?: unknown };
182
206
  if (typeof raw._targetId === "string") return raw._targetId;
@@ -361,6 +385,14 @@ async function clickQueryHandlerText(
361
385
  );
362
386
  }
363
387
 
388
+ interface ActiveRun {
389
+ id: string;
390
+ ac: AbortController;
391
+ displays: RunResultOk["displays"];
392
+ screenshots: ScreenshotResult[];
393
+ pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
394
+ }
395
+
364
396
  export class WorkerCore {
365
397
  #transport: Transport;
366
398
  #browser?: Browser;
@@ -368,7 +400,8 @@ export class WorkerCore {
368
400
  #targetId?: string;
369
401
  #elementCache = new Map<number, ElementHandle>();
370
402
  #elementCounter = 0;
371
- #active?: { id: string; ac: AbortController };
403
+ #active: ActiveRun | null = null;
404
+ #runtime: JsRuntime | null = null;
372
405
  #unsub: () => void;
373
406
  #mode?: WorkerInitPayload["mode"];
374
407
  #dialogPolicy?: DialogPolicy;
@@ -401,6 +434,9 @@ export class WorkerCore {
401
434
  case "abort":
402
435
  if (this.#active?.id === msg.id) this.#active.ac.abort(new ToolAbortError());
403
436
  return;
437
+ case "tool-reply":
438
+ this.#deliverToolReply(msg.id, msg.reply);
439
+ return;
404
440
  case "close":
405
441
  await this.#close();
406
442
  return;
@@ -502,37 +538,26 @@ export class WorkerCore {
502
538
  const timeoutSignal = AbortSignal.timeout(msg.timeoutMs);
503
539
  const ac = new AbortController();
504
540
  const signal = AbortSignal.any([timeoutSignal, ac.signal]);
505
- this.#active = { id: msg.id, ac };
506
541
  const displays: RunResultOk["displays"] = [];
507
542
  const screenshots: ScreenshotResult[] = [];
543
+ const active: ActiveRun = { id: msg.id, ac, displays, screenshots, pendingTools: new Map() };
544
+ this.#active = active;
508
545
  try {
509
546
  throwIfAborted(signal);
510
547
  const page = this.#requirePage();
511
548
  const browser = this.#requireBrowser();
512
549
  const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, displays, screenshots);
513
- const ctx = vm.createContext({
550
+ const runtime = this.#ensureRuntime(msg.session);
551
+ runtime.setCwd(msg.session.cwd);
552
+ runtime.setRunScope({
514
553
  page,
515
554
  browser,
516
555
  tab: tabApi,
517
- display: (value: unknown): void => this.#display(displays, value),
518
556
  assert: (cond: unknown, text?: string): void => {
519
557
  if (!cond) throw new ToolError(text ?? "Assertion failed");
520
558
  },
521
559
  wait: (ms: number): Promise<void> => Bun.sleep(ms),
522
- console: this.#console(),
523
- setTimeout,
524
- clearTimeout,
525
- setInterval,
526
- clearInterval,
527
- queueMicrotask,
528
- Promise,
529
- URL,
530
- URLSearchParams,
531
- TextEncoder,
532
- TextDecoder,
533
- Buffer,
534
560
  });
535
- const wrapped = `(async () => {\n${msg.code}\n})()`;
536
561
  const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
537
562
  const onCancel = (): void => {
538
563
  rejectCancel(
@@ -540,15 +565,17 @@ export class WorkerCore {
540
565
  ? new ToolError(`Browser code execution timed out after ${msg.timeoutMs}ms`)
541
566
  : new ToolAbortError(),
542
567
  );
568
+ // Cancel in-flight tool calls so user code's awaited proxies reject promptly.
569
+ for (const pending of active.pendingTools.values()) {
570
+ pending.reject(new ToolAbortError());
571
+ }
572
+ active.pendingTools.clear();
543
573
  };
544
574
  if (signal.aborted) onCancel();
545
575
  else signal.addEventListener("abort", onCancel, { once: true });
546
576
  try {
547
577
  const returnValue = await Promise.race([
548
- vm.runInContext(wrapped, ctx, {
549
- filename: `browser-run-${msg.id}.js`,
550
- lineOffset: -1,
551
- }) as Promise<unknown>,
578
+ runtime.run(msg.code, `browser-run-${msg.id}.js`),
552
579
  cancelRejection,
553
580
  ]);
554
581
  await this.#postReadyInfo();
@@ -564,8 +591,62 @@ export class WorkerCore {
564
591
  } catch (error) {
565
592
  this.#transport.send({ type: "result", id: msg.id, ok: false, error: errorPayload(error) });
566
593
  } finally {
567
- if (this.#active?.id === msg.id) this.#active = undefined;
594
+ if (this.#active?.id === msg.id) this.#active = null;
595
+ }
596
+ }
597
+
598
+ #ensureRuntime(session: SessionSnapshot): JsRuntime {
599
+ if (this.#runtime) return this.#runtime;
600
+ this.#runtime = new JsRuntime({
601
+ initialCwd: session.cwd,
602
+ sessionId: `browser-tab-${this.#targetId ?? "unknown"}`,
603
+ getHooks: () => this.#hooksForActiveRun(),
604
+ });
605
+ return this.#runtime;
606
+ }
607
+
608
+ #hooksForActiveRun(): RuntimeHooks | null {
609
+ const active = this.#active;
610
+ if (!active) return null;
611
+ return {
612
+ // console.* output stays on the supervisor log channel — matches pre-runtime behavior
613
+ // where browser cells didn't surface `console.log` to the model.
614
+ onText: chunk => this.#log("debug", chunk.replace(/\n$/, "")),
615
+ onDisplay: output => this.#pushDisplay(active.displays, output),
616
+ callTool: (name, args) => this.#callTool(active, name, args),
617
+ };
618
+ }
619
+
620
+ #pushDisplay(displays: RunResultOk["displays"], output: JsDisplayOutput): void {
621
+ if (output.type === "image") {
622
+ displays.push({ type: "image", data: output.data, mimeType: output.mimeType });
623
+ return;
624
+ }
625
+ if (output.type === "json") {
626
+ displays.push({ type: "text", text: safeJsonStringify(output.data) });
627
+ return;
568
628
  }
629
+ // status — surface as compact JSON so helper side effects (read/write/tree) appear in
630
+ // the cell result alongside explicit display() output.
631
+ displays.push({ type: "text", text: safeJsonStringify(output.event) });
632
+ }
633
+
634
+ async #callTool(active: ActiveRun, name: string, args: unknown): Promise<unknown> {
635
+ const id = `tab-tc-${active.id}-${crypto.randomUUID()}`;
636
+ const { promise, resolve, reject } = Promise.withResolvers<unknown>();
637
+ active.pendingTools.set(id, { resolve, reject });
638
+ this.#transport.send({ type: "tool-call", id, runId: active.id, name, args });
639
+ return await promise;
640
+ }
641
+
642
+ #deliverToolReply(id: string, reply: ToolReply): void {
643
+ const active = this.#active;
644
+ if (!active) return;
645
+ const pending = active.pendingTools.get(id);
646
+ if (!pending) return;
647
+ active.pendingTools.delete(id);
648
+ if (reply.ok) pending.resolve(reply.value);
649
+ else pending.reject(replyError(reply.error));
569
650
  }
570
651
 
571
652
  #createTabApi(
@@ -933,41 +1014,6 @@ export class WorkerCore {
933
1014
  }
934
1015
  return handle;
935
1016
  }
936
-
937
- #display(displays: RunResultOk["displays"], value: unknown): void {
938
- if (value === undefined || value === null) return;
939
- if (
940
- typeof value === "object" &&
941
- value !== null &&
942
- "type" in (value as Record<string, unknown>) &&
943
- (value as { type?: unknown }).type === "image"
944
- ) {
945
- const img = value as { data?: unknown; mimeType?: unknown };
946
- if (typeof img.data === "string" && typeof img.mimeType === "string") {
947
- displays.push({ type: "image", data: img.data, mimeType: img.mimeType });
948
- return;
949
- }
950
- }
951
- if (typeof value === "string") {
952
- displays.push({ type: "text", text: value });
953
- return;
954
- }
955
- try {
956
- displays.push({ type: "text", text: JSON.stringify(value, null, 2) });
957
- } catch {
958
- displays.push({ type: "text", text: String(value) });
959
- }
960
- }
961
-
962
- #console(): Pick<Console, "log" | "debug" | "warn" | "error"> {
963
- return {
964
- log: (...args: unknown[]) => this.#log("debug", args.map(String).join(" ")),
965
- debug: (...args: unknown[]) => this.#log("debug", args.map(String).join(" ")),
966
- warn: (...args: unknown[]) => this.#log("warn", args.map(String).join(" ")),
967
- error: (...args: unknown[]) => this.#log("error", args.map(String).join(" ")),
968
- };
969
- }
970
-
971
1017
  #clearElementCache(): void {
972
1018
  if (this.#elementCache.size === 0) {
973
1019
  this.#elementCounter = 0;