@oh-my-pi/pi-coding-agent 16.1.13 → 16.1.14

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 (129) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5547 -2759
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  5. package/dist/types/eval/backend-helpers.d.ts +23 -0
  6. package/dist/types/eval/executor-base.d.ts +118 -0
  7. package/dist/types/eval/index.d.ts +2 -0
  8. package/dist/types/eval/jl/executor.d.ts +44 -0
  9. package/dist/types/eval/jl/index.d.ts +11 -0
  10. package/dist/types/eval/jl/kernel.d.ts +28 -0
  11. package/dist/types/eval/jl/prelude.d.ts +1 -0
  12. package/dist/types/eval/jl/runtime.d.ts +22 -0
  13. package/dist/types/eval/kernel-base.d.ts +105 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -61
  15. package/dist/types/eval/rb/executor.d.ts +77 -0
  16. package/dist/types/eval/rb/index.d.ts +11 -0
  17. package/dist/types/eval/rb/kernel.d.ts +31 -0
  18. package/dist/types/eval/rb/prelude.d.ts +1 -0
  19. package/dist/types/eval/rb/runtime.d.ts +23 -0
  20. package/dist/types/eval/runtime-env.d.ts +24 -0
  21. package/dist/types/eval/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  23. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  24. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  25. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  26. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  27. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  28. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  29. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  30. package/dist/types/modes/interactive-mode.d.ts +0 -2
  31. package/dist/types/modes/theme/theme.d.ts +8 -1
  32. package/dist/types/modes/types.d.ts +0 -2
  33. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  34. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  35. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +2 -2
  38. package/dist/types/stt/asr-client.d.ts +3 -29
  39. package/dist/types/subprocess/worker-client.d.ts +149 -0
  40. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  41. package/dist/types/tiny/title-client.d.ts +14 -34
  42. package/dist/types/tools/eval-backends.d.ts +6 -3
  43. package/dist/types/tools/eval-render.d.ts +6 -5
  44. package/dist/types/tools/eval.d.ts +13 -15
  45. package/dist/types/tools/index.d.ts +3 -3
  46. package/dist/types/tts/tts-client.d.ts +3 -28
  47. package/dist/types/tui/code-cell.d.ts +7 -0
  48. package/dist/types/utils/file-display-mode.d.ts +1 -1
  49. package/dist/types/web/parallel.d.ts +6 -0
  50. package/package.json +12 -12
  51. package/src/config/settings-schema.ts +50 -18
  52. package/src/config/settings.ts +5 -0
  53. package/src/dap/client.ts +13 -107
  54. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  55. package/src/eval/backend-helpers.ts +48 -0
  56. package/src/eval/executor-base.ts +425 -0
  57. package/src/eval/index.ts +2 -0
  58. package/src/eval/jl/executor.ts +540 -0
  59. package/src/eval/jl/index.ts +54 -0
  60. package/src/eval/jl/kernel.ts +235 -0
  61. package/src/eval/jl/prelude.jl +930 -0
  62. package/src/eval/jl/prelude.ts +3 -0
  63. package/src/eval/jl/runner.jl +634 -0
  64. package/src/eval/jl/runtime.ts +118 -0
  65. package/src/eval/js/index.ts +3 -14
  66. package/src/eval/kernel-base.ts +569 -0
  67. package/src/eval/py/executor.ts +43 -252
  68. package/src/eval/py/index.ts +9 -20
  69. package/src/eval/py/kernel.ts +29 -544
  70. package/src/eval/rb/executor.ts +504 -0
  71. package/src/eval/rb/index.ts +54 -0
  72. package/src/eval/rb/kernel.ts +230 -0
  73. package/src/eval/rb/prelude.rb +721 -0
  74. package/src/eval/rb/prelude.ts +3 -0
  75. package/src/eval/rb/runner.rb +474 -0
  76. package/src/eval/rb/runtime.ts +132 -0
  77. package/src/eval/runtime-env.ts +104 -0
  78. package/src/eval/types.ts +3 -3
  79. package/src/extensibility/extensions/runner.ts +4 -11
  80. package/src/extensibility/hooks/loader.ts +3 -21
  81. package/src/extensibility/session-handler-types.ts +21 -0
  82. package/src/internal-urls/docs-index.generated.txt +1 -1
  83. package/src/jsonrpc/message-framing.ts +142 -0
  84. package/src/lsp/client.ts +13 -109
  85. package/src/mnemopi/embed-client.ts +43 -198
  86. package/src/modes/components/agent-dashboard.ts +17 -40
  87. package/src/modes/components/chat-transcript-builder.ts +18 -102
  88. package/src/modes/components/custom-editor.ts +19 -0
  89. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  90. package/src/modes/components/extensions/extension-list.ts +16 -44
  91. package/src/modes/components/history-search.ts +4 -16
  92. package/src/modes/components/selector-helpers.ts +129 -0
  93. package/src/modes/components/settings-selector.ts +7 -5
  94. package/src/modes/components/tree-selector.ts +13 -18
  95. package/src/modes/controllers/event-controller.ts +3 -9
  96. package/src/modes/controllers/input-controller.ts +32 -54
  97. package/src/modes/interactive-mode.ts +5 -7
  98. package/src/modes/theme/theme.ts +35 -0
  99. package/src/modes/types.ts +0 -2
  100. package/src/modes/utils/copy-targets.ts +3 -2
  101. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  102. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  103. package/src/modes/utils/ui-helpers.ts +21 -126
  104. package/src/prompts/system/system-prompt.md +10 -10
  105. package/src/prompts/tools/bash.md +0 -1
  106. package/src/prompts/tools/eval.md +6 -4
  107. package/src/prompts/tools/find.md +0 -4
  108. package/src/prompts/tools/read.md +1 -2
  109. package/src/prompts/tools/replace.md +1 -1
  110. package/src/prompts/tools/search.md +0 -1
  111. package/src/sdk.ts +13 -7
  112. package/src/session/agent-session.ts +9 -7
  113. package/src/stt/asr-client.ts +35 -215
  114. package/src/stt/asr-worker.ts +29 -181
  115. package/src/subprocess/worker-client.ts +297 -0
  116. package/src/subprocess/worker-runtime.ts +277 -0
  117. package/src/task/executor.ts +4 -4
  118. package/src/tiny/title-client.ts +53 -219
  119. package/src/tiny/worker.ts +29 -180
  120. package/src/tools/eval-backends.ts +10 -3
  121. package/src/tools/eval-render.ts +17 -8
  122. package/src/tools/eval.ts +187 -22
  123. package/src/tools/index.ts +51 -28
  124. package/src/tts/tts-client.ts +38 -206
  125. package/src/tts/tts-worker.ts +23 -97
  126. package/src/tui/code-cell.ts +12 -1
  127. package/src/utils/file-display-mode.ts +2 -3
  128. package/src/web/parallel.ts +43 -42
  129. package/src/web/search/providers/parallel.ts +10 -99
@@ -1,7 +1,18 @@
1
- import * as path from "node:path";
2
- import { $env, isBunTestRuntime, isCompiledBinary, logger, workerHostEntry } from "@oh-my-pi/pi-utils";
3
- import type { Subprocess } from "bun";
1
+ import { $env, logger } from "@oh-my-pi/pi-utils";
4
2
  import { settings } from "../config/settings";
3
+ import {
4
+ createUnavailableWorker,
5
+ createWorkerHandle,
6
+ createWorkerSubprocess,
7
+ logWorkerMessage,
8
+ resolveWorkerSpawnCmd,
9
+ SMOKE_TEST_TIMEOUT_MS,
10
+ type SpawnedSubprocess,
11
+ smokeTestWorker,
12
+ spawnWorkerOrUnavailable,
13
+ type WorkerHandle,
14
+ workerEnvFromParent,
15
+ } from "../subprocess/worker-client";
5
16
  import { safeSend } from "../utils/ipc";
6
17
  import { tinyModelDeviceSettingToEnv } from "./device";
7
18
  import { tinyModelDtypeSettingToEnv } from "./dtype";
@@ -15,21 +26,6 @@ import {
15
26
  } from "./models";
16
27
  import type { TinyTitleProgressEvent, TinyTitleWorkerInbound, TinyTitleWorkerOutbound } from "./title-protocol";
17
28
 
18
- /**
19
- * Abstraction over the tiny-model subprocess. Modelled as a worker interface
20
- * so existing callers (titles, memory completions, downloads) compose the
21
- * same way; the runtime implementation is a Bun child process so
22
- * `onnxruntime-node`'s NAPI finalizer never runs inside the main agent
23
- * address space — that destructor segfaults Bun on Windows during shutdown
24
- * (issue #1606).
25
- */
26
- interface WorkerHandle {
27
- send(message: TinyTitleWorkerInbound): void;
28
- onMessage(handler: (message: TinyTitleWorkerOutbound) => void): () => void;
29
- onError(handler: (error: Error) => void): () => void;
30
- terminate(): Promise<void>;
31
- }
32
-
33
29
  type PendingRequest =
34
30
  | { kind: "generate"; modelKey: TinyTitleLocalModelKey; resolve: (title: string | null) => void }
35
31
  | { kind: "complete"; modelKey: TinyMemoryLocalModelKey; resolve: (text: string | null) => void }
@@ -51,13 +47,6 @@ export interface TinyTitleGenerateOptions {
51
47
  systemPrompt?: string;
52
48
  }
53
49
 
54
- // Cold-starting the worker subprocess from a compiled binary (decompress + module
55
- // graph load) is slow on contended CI runners — the macos-15-intel release smoke
56
- // blew past 5s while arm64/linux/win passed. The probe only needs to prove the
57
- // worker spawns and ponges at all (a dead worker never ponges regardless), so a
58
- // generous bound removes the flake without weakening the check.
59
- const SMOKE_TEST_TIMEOUT_MS = 30_000;
60
-
61
50
  function normalizeTinyTitleGenerateOptions(
62
51
  options: AbortSignal | TinyTitleGenerateOptions | undefined,
63
52
  ): TinyTitleGenerateOptions {
@@ -107,201 +96,66 @@ export function tinyWorkerEnvOverlay(
107
96
  }
108
97
 
109
98
  /**
110
- * Env handed to the tiny-model subprocess. The `PI_TINY_DEVICE` / `PI_TINY_DTYPE`
111
- * env vars win; otherwise the persisted `providers.tinyModelDevice` /
112
- * `providers.tinyModelDtype` settings are mapped onto those vars so the
113
- * subprocess's env-based resolution picks them up. Resolved once at spawn
114
- * (pipelines are cached for the lifetime of the subprocess).
99
+ * Env handed to the tiny-model subprocess and reused verbatim by the STT and
100
+ * TTS workers, which share the same device/dtype resolution. The
101
+ * `PI_TINY_DEVICE` / `PI_TINY_DTYPE` env vars win; otherwise the persisted
102
+ * `providers.tinyModelDevice` / `providers.tinyModelDtype` settings are mapped
103
+ * onto those vars so the subprocess's env-based resolution picks them up.
104
+ * Resolved once at spawn (pipelines are cached for the lifetime of the
105
+ * subprocess).
115
106
  */
116
- function tinyWorkerEnv(): Record<string, string> {
117
- const overlay = tinyWorkerEnvOverlay(
118
- $env,
119
- readTinyModelSetting("providers.tinyModelDevice"),
120
- readTinyModelSetting("providers.tinyModelDtype"),
107
+ export function tinyWorkerEnv(): Record<string, string> {
108
+ return workerEnvFromParent(
109
+ tinyWorkerEnvOverlay(
110
+ $env,
111
+ readTinyModelSetting("providers.tinyModelDevice"),
112
+ readTinyModelSetting("providers.tinyModelDtype"),
113
+ ),
121
114
  );
122
- const base = $env as Record<string, string | undefined>;
123
- const merged: Record<string, string> = {};
124
- for (const key in base) {
125
- const value = base[key];
126
- if (typeof value === "string") merged[key] = value;
127
- }
128
- for (const key in overlay) merged[key] = overlay[key];
129
- return merged;
130
- }
131
- interface TinyWorkerSpawnCommand {
132
- cmd: string[];
133
- cwd?: string;
134
- }
135
-
136
- /**
137
- * Resolve the command used to relaunch the agent CLI into tiny-worker mode.
138
- * In a compiled binary the entry point is the binary itself (no script arg).
139
- * Otherwise re-enter the declared worker-host entry (source cli.ts or
140
- * npm-bundle cli.js) with a cwd-relative script path — Bun's subprocess IPC
141
- * is more reliable that way than with an absolute `.ts` entry under
142
- * `bun test` — and fall back to this package's own `src/cli.ts` when no host
143
- * entry is declared (bun test, SDK embedding).
144
- */
145
- function tinyWorkerSpawnCmd(): TinyWorkerSpawnCommand {
146
- if (isCompiledBinary()) return { cmd: [process.execPath, TINY_WORKER_ARG] };
147
- const hostEntry = workerHostEntry();
148
- if (hostEntry) {
149
- return { cmd: [process.execPath, path.basename(hostEntry), TINY_WORKER_ARG], cwd: path.dirname(hostEntry) };
150
- }
151
- const packageRoot = path.resolve(import.meta.dir, "..", "..");
152
- return { cmd: [process.execPath, "src/cli.ts", TINY_WORKER_ARG], cwd: packageRoot };
153
- }
154
-
155
- interface SpawnedSubprocess {
156
- proc: Subprocess<"ignore", "ignore", "ignore">;
157
- inbound: Set<(message: TinyTitleWorkerOutbound) => void>;
158
- errors: Set<(error: Error) => void>;
159
- /**
160
- * Flipped to `true` by {@link wrapSubprocess}'s `terminate()` right
161
- * before it SIGKILLs the child so `onExit` can distinguish the
162
- * expected hard-kill from a crash/OOM/external signal. Only the
163
- * latter is surfaced as a worker error.
164
- */
165
- intentionalExit: { value: boolean };
166
115
  }
167
116
 
168
117
  /**
169
118
  * Spawn the tiny-model worker as a subprocess. Exported for tests and the
170
- * smoke probe; production callers go through {@link spawnTinyTitleWorker}
171
- * which wraps the result in a {@link WorkerHandle}.
119
+ * smoke probe; production callers go through {@link spawnTinyTitleWorker}.
172
120
  */
173
- export function createTinyTitleSubprocess(): SpawnedSubprocess {
174
- const inbound = new Set<(message: TinyTitleWorkerOutbound) => void>();
175
- const errors = new Set<(error: Error) => void>();
176
- const intentionalExit = { value: false };
177
- const spawnCommand = tinyWorkerSpawnCmd();
178
- const proc = Bun.spawn({
179
- cmd: spawnCommand.cmd,
180
- cwd: spawnCommand.cwd,
121
+ export function createTinyTitleSubprocess(): SpawnedSubprocess<TinyTitleWorkerOutbound> {
122
+ return createWorkerSubprocess<TinyTitleWorkerOutbound>({
123
+ spawnCommand: resolveWorkerSpawnCmd(TINY_WORKER_ARG),
181
124
  env: tinyWorkerEnv(),
182
- stdin: "ignore",
183
- stdout: "ignore",
184
- stderr: "ignore",
185
- serialization: "advanced",
186
- windowsHide: true,
187
- // The worker is an implementation detail of the interactive TUI. Native
188
- // model runtimes may print progress or decoded text directly; never let
189
- // those bytes inherit the terminal and corrupt the chat scrollback.
190
- ipc(message) {
191
- for (const handler of inbound) handler(message as TinyTitleWorkerOutbound);
192
- },
193
- onExit(_proc, exitCode, signalCode) {
194
- // Clean exit. The child only exits via SIGKILL in practice, but
195
- // treat code 0 as a no-op for symmetry.
196
- if (exitCode === 0) return;
197
- // `exitCode === null` + non-null `signalCode` covers both the
198
- // expected SIGKILL from `terminate()` AND external kills
199
- // (SIGSEGV from a native crash, SIGKILL from the OOM killer, an
200
- // operator `kill -9`, etc.). Swallow only the expected one;
201
- // every other signal exit is a real worker death that must
202
- // fault every in-flight request so callers don't await forever.
203
- if (exitCode === null && intentionalExit.value) return;
204
- const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
205
- const err = new Error(`tiny model subprocess exited with ${reason}`);
206
- for (const handler of errors) handler(err);
207
- },
125
+ exitLabel: "tiny model subprocess",
208
126
  });
209
- // Don't keep the parent event loop alive on account of an idle worker; the
210
- // agent dispose path calls `terminate()` explicitly when shutting down.
211
- // Bun's test runner can starve IPC delivery for unref'd subprocesses, so
212
- // keep it referenced only under tests that assert the ping/pong contract.
213
- if (!isBunTestRuntime()) proc.unref();
214
- return { proc, inbound, errors, intentionalExit };
215
127
  }
216
128
 
217
- function wrapSubprocess({ proc, inbound, errors, intentionalExit }: SpawnedSubprocess): WorkerHandle {
218
- return {
219
- send(message) {
220
- safeSend(proc, message, "tiny-title");
221
- },
222
- onMessage(handler) {
223
- inbound.add(handler);
224
- return () => inbound.delete(handler);
225
- },
226
- onError(handler) {
227
- errors.add(handler);
228
- return () => errors.delete(handler);
229
- },
230
- async terminate() {
231
- // SIGKILL: the whole point of the subprocess isolation is that the
232
- // parent never runs `onnxruntime-node`'s NAPI finalizer. A polite
233
- // SIGTERM lets the subprocess try to clean up, which is exactly the
234
- // codepath that crashes Bun on Windows. Hard-kill instead — the
235
- // model lives in process memory and the OS reclaims everything.
236
- // Flip the intentional-exit flag *before* killing so `onExit` can
237
- // tell this apart from a crash or external SIGKILL.
238
- intentionalExit.value = true;
239
- try {
240
- proc.kill("SIGKILL");
241
- } catch {
242
- // Already gone.
243
- }
244
- },
245
- };
246
- }
247
-
248
- function spawnInlineUnavailableWorker(error: unknown): WorkerHandle {
249
- const listeners = new Set<(message: TinyTitleWorkerOutbound) => void>();
250
- const errorMessage = error instanceof Error ? error.message : String(error);
251
- const emit = (message: TinyTitleWorkerOutbound): void => {
252
- for (const listener of listeners) listener(message);
253
- };
254
- return {
255
- send(message) {
256
- queueMicrotask(() => {
257
- if (message.type === "ping") {
258
- emit({ type: "pong", id: message.id });
259
- return;
260
- }
261
- emit({ type: "error", id: message.id, error: errorMessage });
262
- });
263
- },
264
- onMessage(handler) {
265
- listeners.add(handler);
266
- return () => listeners.delete(handler);
267
- },
268
- onError() {
269
- return () => {};
270
- },
271
- async terminate() {
272
- listeners.clear();
273
- },
274
- };
275
- }
276
-
277
- function spawnTinyTitleWorker(): WorkerHandle {
278
- try {
279
- return wrapSubprocess(createTinyTitleSubprocess());
280
- } catch (error) {
281
- logger.warn("Tiny title worker spawn failed; local titles disabled", {
282
- error: error instanceof Error ? error.message : String(error),
283
- });
284
- return spawnInlineUnavailableWorker(error);
285
- }
129
+ function wrapSubprocess(
130
+ spawned: SpawnedSubprocess<TinyTitleWorkerOutbound>,
131
+ ): WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
132
+ const { proc } = spawned;
133
+ return createWorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>(spawned, message =>
134
+ safeSend(proc, message, "tiny-title"),
135
+ );
286
136
  }
287
137
 
288
- function logWorkerMessage(message: Extract<TinyTitleWorkerOutbound, { type: "log" }>): void {
289
- if (message.level === "debug") logger.debug(message.msg, message.meta);
290
- else if (message.level === "warn") logger.warn(message.msg, message.meta);
291
- else logger.error(message.msg, message.meta);
138
+ function spawnTinyTitleWorker(): WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
139
+ return spawnWorkerOrUnavailable(
140
+ () => wrapSubprocess(createTinyTitleSubprocess()),
141
+ createUnavailableWorker<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>,
142
+ "Tiny title worker spawn failed; local titles disabled",
143
+ );
292
144
  }
293
145
 
294
146
  export class TinyTitleClient {
295
- #worker: WorkerHandle | null = null;
147
+ #worker: WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> | null = null;
296
148
  #unsubscribeMessage: (() => void) | null = null;
297
149
  #unsubscribeError: (() => void) | null = null;
298
150
  #pending = new Map<string, PendingRequest>();
299
151
  #failedModels = new Set<TinyLocalModelKey>();
300
152
  #progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
301
153
  #nextRequestId = 0;
302
- #spawnWorker: () => WorkerHandle;
154
+ #spawnWorker: () => WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound>;
303
155
 
304
- constructor(spawnWorker: () => WorkerHandle = spawnTinyTitleWorker) {
156
+ constructor(
157
+ spawnWorker: () => WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> = spawnTinyTitleWorker,
158
+ ) {
305
159
  this.#spawnWorker = spawnWorker;
306
160
  }
307
161
 
@@ -443,7 +297,7 @@ export class TinyTitleClient {
443
297
  }
444
298
  }
445
299
 
446
- #ensureWorker(): WorkerHandle {
300
+ #ensureWorker(): WorkerHandle<TinyTitleWorkerInbound, TinyTitleWorkerOutbound> {
447
301
  if (this.#worker) return this.#worker;
448
302
  const worker = this.#spawnWorker();
449
303
  this.#worker = worker;
@@ -520,25 +374,5 @@ export async function smokeTestTinyTitleWorker({
520
374
  }: {
521
375
  timeoutMs?: number;
522
376
  } = {}): Promise<void> {
523
- const handle = wrapSubprocess(createTinyTitleSubprocess());
524
- const { promise, resolve, reject } = Promise.withResolvers<void>();
525
- const timer = setTimeout(() => reject(new Error(`tiny title worker did not pong within ${timeoutMs}ms`)), timeoutMs);
526
- const unsubscribeMessage = handle.onMessage(message => {
527
- if (message.type === "pong") {
528
- resolve();
529
- return;
530
- }
531
- if (message.type === "log") return;
532
- reject(new Error(`tiny title worker: expected pong, got ${JSON.stringify(message)}`));
533
- });
534
- const unsubscribeError = handle.onError(reject);
535
- try {
536
- handle.send({ type: "ping", id: "smoke" } satisfies TinyTitleWorkerInbound);
537
- await promise;
538
- } finally {
539
- clearTimeout(timer);
540
- unsubscribeMessage();
541
- unsubscribeError();
542
- await handle.terminate();
543
- }
377
+ await smokeTestWorker(wrapSubprocess(createTinyTitleSubprocess()), "tiny title worker", timeoutMs);
544
378
  }
@@ -1,4 +1,3 @@
1
- import { createRequire } from "node:module";
2
1
  import * as path from "node:path";
3
2
  import type {
4
3
  ProgressInfo,
@@ -6,16 +5,18 @@ import type {
6
5
  TextGenerationStringOutput,
7
6
  StoppingCriteria as TransformersStoppingCriteria,
8
7
  } from "@huggingface/transformers";
9
- import {
10
- ensureRuntimeInstalled,
11
- getTinyModelsCacheDir,
12
- installRuntimeModuleResolver,
13
- isCompiledBinary,
14
- prompt,
15
- resolveRuntimeModule,
16
- } from "@oh-my-pi/pi-utils";
17
- import packageJson from "../../package.json" with { type: "json" };
8
+ import { getTinyModelsCacheDir, prompt } from "@oh-my-pi/pi-utils";
18
9
  import tinyTitleSystemPrompt from "../prompts/system/tiny-title-system.md" with { type: "text" };
10
+ import {
11
+ errorMessage,
12
+ errorText,
13
+ getTransformersVersionSpec,
14
+ loadTransformersRuntime,
15
+ MemoizedRuntime,
16
+ replayCachedReady,
17
+ sendLog,
18
+ sendProgress,
19
+ } from "../subprocess/worker-runtime";
19
20
  import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
20
21
  import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
21
22
  import {
@@ -25,7 +26,7 @@ import {
25
26
  type TinyTitleLocalModelSpec,
26
27
  } from "./models";
27
28
  import { formatTitleUserMessage, normalizeGeneratedTitle } from "./text";
28
- import type { TinyTitleProgressEvent, TinyTitleTransport, TinyTitleWorkerInbound } from "./title-protocol";
29
+ import type { TinyTitleTransport, TinyTitleWorkerInbound } from "./title-protocol";
29
30
 
30
31
  const TITLE_PREFILL = "<title>";
31
32
  const TITLE_CLOSE = "</title>";
@@ -34,9 +35,6 @@ const STOP_DECODE_WINDOW_TOKENS = 32;
34
35
  const MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS = 256;
35
36
  const COMPLETION_MAX_NEW_TOKENS = 1024;
36
37
  const TINY_TITLE_SYSTEM_PROMPT = prompt.render(tinyTitleSystemPrompt);
37
- const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
38
- const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
39
- const sourceRequire = createRequire(import.meta.url);
40
38
 
41
39
  const tinyModelDevicePreference = resolveTinyModelDevicePreference();
42
40
  const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
@@ -64,51 +62,11 @@ interface TransformersRuntime {
64
62
 
65
63
  const pipelines = new Map<TinyLocalModelKey, Promise<TextGenerationPipeline>>();
66
64
 
67
- function resolveTransformersVersionSpec(): string {
68
- const manifest = packageJson as {
69
- optionalDependencies?: Record<string, string>;
70
- dependencies?: Record<string, string>;
71
- };
72
- const versionSpec =
73
- manifest.optionalDependencies?.[TRANSFORMERS_PACKAGE] ?? manifest.dependencies?.[TRANSFORMERS_PACKAGE];
74
- if (!versionSpec) throw new Error(`${TRANSFORMERS_PACKAGE} is missing from package.json optionalDependencies`);
75
- if (!versionSpec.startsWith("catalog:")) return versionSpec;
76
- if (COMPILED_TRANSFORMERS_VERSION) return COMPILED_TRANSFORMERS_VERSION;
77
- const installed = sourceRequire(`${TRANSFORMERS_PACKAGE}/package.json`) as { version: string };
78
- return installed.version;
79
- }
80
- let cachedTransformersVersionSpec: string | undefined;
81
- /**
82
- * Lazily resolve (and memoize) the transformers version spec. In the
83
- * `catalog:` case {@link resolveTransformersVersionSpec} `require`s the
84
- * installed `@huggingface/transformers/package.json`, so touching it forces
85
- * the dependency to exist. Defer it to the compiled-binary runtime-install
86
- * path — which only runs when a local title model is actually generated or
87
- * downloaded — so loading this worker (smoke-test ping, online title path)
88
- * never triggers the transformers resolve/install dance.
89
- */
90
- function getTransformersVersionSpec(): string {
91
- cachedTransformersVersionSpec ??= resolveTransformersVersionSpec();
92
- return cachedTransformersVersionSpec;
93
- }
94
65
  function getTransformersRuntimeKey(): string {
95
66
  return getTransformersVersionSpec().replace(/[^A-Za-z0-9._-]/g, "_");
96
67
  }
97
68
  let generateQueue = Promise.resolve();
98
- let transformersRuntime: Promise<TransformersRuntime> | null = null;
99
-
100
- function errorText(error: unknown): string {
101
- return error instanceof Error ? (error.stack ?? error.message) : String(error);
102
- }
103
-
104
- function sendLog(
105
- transport: TinyTitleTransport,
106
- level: "debug" | "warn" | "error",
107
- msg: string,
108
- meta?: Record<string, unknown>,
109
- ): void {
110
- transport.send({ type: "log", level, msg, meta });
111
- }
69
+ const transformersRuntime = new MemoizedRuntime<TransformersRuntime>();
112
70
 
113
71
  function getTinyTitleRuntimeDir(): string {
114
72
  return path.join(
@@ -118,74 +76,6 @@ function getTinyTitleRuntimeDir(): string {
118
76
  );
119
77
  }
120
78
 
121
- function sendRuntimeInstallProgress(
122
- transport: TinyTitleTransport,
123
- requestId: string,
124
- modelKey: TinyLocalModelKey,
125
- status: "initiate" | "download" | "done",
126
- ): void {
127
- transport.send({
128
- type: "progress",
129
- id: requestId,
130
- event: {
131
- modelKey,
132
- status,
133
- name: `${TRANSFORMERS_PACKAGE}@${getTransformersVersionSpec()}`,
134
- },
135
- });
136
- }
137
-
138
- /**
139
- * Prepare the freshly-installed compiled runtime for loading: stub `sharp`
140
- * (the tiny models are text-generation only, so the native image pipeline is
141
- * dead weight) and patch the module resolver so Transformers.js's bare requires
142
- * (`onnxruntime-node`, `onnxruntime-common`) resolve against the cache. Returns
143
- * the absolute Transformers.js entrypoint to `require`.
144
- */
145
- async function prepareCompiledRuntime(runtimeDir: string): Promise<string> {
146
- const nodeModules = path.join(runtimeDir, "node_modules");
147
- const sharpStub = path.join(runtimeDir, "omp-sharp-stub.cjs");
148
- await Bun.write(sharpStub, "module.exports = {};\n");
149
- installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
150
- const entry = resolveRuntimeModule(nodeModules, TRANSFORMERS_PACKAGE);
151
- if (!entry) throw new Error(`Unable to resolve ${TRANSFORMERS_PACKAGE} in compiled runtime at ${nodeModules}`);
152
- return entry;
153
- }
154
-
155
- function configureTransformers(transformers: TransformersRuntime): TransformersRuntime {
156
- transformers.env.cacheDir = getTinyModelsCacheDir();
157
- transformers.env.allowLocalModels = false;
158
- transformers.env.logLevel = transformers.LogLevel.ERROR;
159
- return transformers;
160
- }
161
-
162
- async function loadTransformers(
163
- transport: TinyTitleTransport,
164
- requestId: string,
165
- modelKey: TinyLocalModelKey,
166
- ): Promise<TransformersRuntime> {
167
- if (transformersRuntime) return transformersRuntime;
168
- transformersRuntime = (async () => {
169
- if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as TransformersRuntime);
170
- const runtimeDir = await ensureRuntimeInstalled({
171
- runtimeDir: getTinyTitleRuntimeDir(),
172
- install: {
173
- dependencies: { [TRANSFORMERS_PACKAGE]: getTransformersVersionSpec() },
174
- trustedDependencies: ["onnxruntime-node"],
175
- },
176
- probePackage: TRANSFORMERS_PACKAGE,
177
- onPhase: phase => sendRuntimeInstallProgress(transport, requestId, modelKey, phase),
178
- });
179
- const entry = await prepareCompiledRuntime(runtimeDir);
180
- const require_ = createRequire(entry);
181
- return configureTransformers(require_(entry) as TransformersRuntime);
182
- })().catch(error => {
183
- transformersRuntime = null;
184
- throw error;
185
- });
186
- return transformersRuntime;
187
- }
188
-
189
79
  function createStopOnTextCriteria(
190
80
  transformers: TransformersRuntime,
191
81
  tokenizer: TextGenerationPipeline["tokenizer"],
@@ -215,48 +105,6 @@ function createStopOnTextCriteria(
215
105
  return new StopOnTextCriteria();
216
106
  }
217
107
 
218
- function toProgressEvent(modelKey: TinyLocalModelKey, info: ProgressInfo): TinyTitleProgressEvent {
219
- if (info.status === "ready") {
220
- return { modelKey, status: info.status, task: info.task, model: info.model };
221
- }
222
- if (info.status === "progress_total") {
223
- return {
224
- modelKey,
225
- status: info.status,
226
- name: info.name,
227
- progress: info.progress,
228
- loaded: info.loaded,
229
- total: info.total,
230
- files: info.files,
231
- };
232
- }
233
- if (info.status === "progress") {
234
- return {
235
- modelKey,
236
- status: info.status,
237
- name: info.name,
238
- file: info.file,
239
- progress: info.progress,
240
- loaded: info.loaded,
241
- total: info.total,
242
- };
243
- }
244
- return { modelKey, status: info.status, name: info.name, file: info.file };
245
- }
246
-
247
- function sendProgress(
248
- transport: TinyTitleTransport,
249
- id: string,
250
- modelKey: TinyLocalModelKey,
251
- info: ProgressInfo,
252
- ): void {
253
- transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
254
- }
255
-
256
- function errorMessage(error: unknown): string {
257
- return error instanceof Error ? error.message : String(error);
258
- }
259
-
260
108
  async function loadPipelineOnDevice(
261
109
  transformers: TransformersRuntime,
262
110
  spec: TinyTitleLocalModelSpec,
@@ -318,21 +166,16 @@ async function loadPipeline(
318
166
  const spec = getTinyLocalModelSpec(modelKey);
319
167
  if (!spec) throw new Error(`Unknown tiny local model: ${modelKey}`);
320
168
  if (spec.unsupportedReason) throw new Error(`${modelKey} is unavailable: ${spec.unsupportedReason}`);
321
- const cached = pipelines.get(modelKey);
322
- if (cached) {
323
- void cached
324
- .then(() => {
325
- transport.send({
326
- type: "progress",
327
- id: requestId,
328
- event: { modelKey, status: "ready", task: "text-generation", model: spec.repo },
329
- });
330
- })
331
- .catch(() => undefined);
332
- return cached;
333
- }
169
+ const cached = replayCachedReady(pipelines, modelKey, transport, requestId, "text-generation", spec.repo);
170
+ if (cached) return cached;
334
171
 
335
- const transformers = await loadTransformers(transport, requestId, modelKey);
172
+ const transformers = await loadTransformersRuntime(
173
+ transformersRuntime,
174
+ transport,
175
+ requestId,
176
+ modelKey,
177
+ getTinyTitleRuntimeDir,
178
+ );
336
179
  const startedAt = performance.now();
337
180
  const loaded = loadPipelineWithDeviceFallback(transformers, spec, modelKey, transport, requestId).then(
338
181
  ({ generator, device }) => {
@@ -393,7 +236,13 @@ async function generateTitle(
393
236
  ): Promise<string | null> {
394
237
  const generator = await loadPipeline(modelKey, transport, requestId);
395
238
  const promptText = buildPrompt(generator, message, systemPrompt);
396
- const transformers = await loadTransformers(transport, requestId, modelKey);
239
+ const transformers = await loadTransformersRuntime(
240
+ transformersRuntime,
241
+ transport,
242
+ requestId,
243
+ modelKey,
244
+ getTinyTitleRuntimeDir,
245
+ );
397
246
  const output = (await generator(promptText, {
398
247
  max_new_tokens: TITLE_MAX_NEW_TOKENS,
399
248
  do_sample: false,
@@ -4,24 +4,31 @@ import type { ToolSession } from ".";
4
4
  export interface EvalBackendsAllowance {
5
5
  python: boolean;
6
6
  js: boolean;
7
+ ruby: boolean;
8
+ julia: boolean;
7
9
  }
8
10
 
9
- /** Read per-backend allowance from settings (defaults true). */
11
+ /** Read per-backend allowance from settings (py/js default on; rb/jl opt-in, default off). */
10
12
  export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
11
13
  return {
12
14
  python: session.settings.get("eval.py") ?? true,
13
15
  js: session.settings.get("eval.js") ?? true,
16
+ ruby: session.settings.get("eval.rb") ?? false,
17
+ julia: session.settings.get("eval.jl") ?? false,
14
18
  };
15
19
  }
16
20
 
17
21
  /**
18
- * Materialize the active eval backend allowance: PI_PY / PI_JS env flags
19
- * override the per-key settings; otherwise settings (defaults true) win.
22
+ * Materialize the active eval backend allowance: PI_PY / PI_JS / PI_RB / PI_JL
23
+ * env flags override the per-key settings; otherwise settings win (py/js default
24
+ * on, rb/jl default off).
20
25
  */
21
26
  export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
22
27
  const settings = readEvalBackendsAllowance(session);
23
28
  return {
24
29
  python: $flag("PI_PY", settings.python),
25
30
  js: $flag("PI_JS", settings.js),
31
+ ruby: $flag("PI_RB", settings.ruby),
32
+ julia: $flag("PI_JL", settings.julia),
26
33
  };
27
34
  }