@oh-my-pi/pi-coding-agent 16.1.12 → 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.
- package/CHANGELOG.md +27 -0
- package/dist/cli.js +5650 -2860
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +0 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
|
@@ -0,0 +1,297 @@
|
|
|
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";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shared lifecycle scaffolding for the ONNX inference subprocess clients
|
|
7
|
+
* (mnemopi embeddings, speech-to-text, tiny-model titles/completions, TTS).
|
|
8
|
+
* Each runs `onnxruntime-node` inside a dedicated Bun child process so the NAPI
|
|
9
|
+
* constructor/finalizer never executes in the main agent address space — those
|
|
10
|
+
* destructors segfault Bun on shutdown (issues #1606 / #1607 / #3031).
|
|
11
|
+
*
|
|
12
|
+
* Only the genuinely identical pieces live here: the worker-handle shape, the
|
|
13
|
+
* spawn-command resolution, the parent-env snapshot, the `Bun.spawn` wiring,
|
|
14
|
+
* the inline "worker unavailable" stub, and the ping/pong smoke probe. Each
|
|
15
|
+
* client keeps its own divergent request/response correlation, streaming, and
|
|
16
|
+
* teardown semantics.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Minimal inbound contract shared by every worker: a correlated `ping`. */
|
|
20
|
+
export type WorkerInboundBase = { type: "ping"; id: string };
|
|
21
|
+
|
|
22
|
+
/** Structured log line forwarded from a worker to the parent logger. */
|
|
23
|
+
export type WorkerLogMessage = {
|
|
24
|
+
type: "log";
|
|
25
|
+
level: "debug" | "warn" | "error";
|
|
26
|
+
msg: string;
|
|
27
|
+
meta?: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Minimal outbound contract shared by every worker: `pong`, `error`, `log`. */
|
|
31
|
+
export type WorkerOutboundBase =
|
|
32
|
+
| { type: "pong"; id: string }
|
|
33
|
+
| { type: "error"; id: string; error: string }
|
|
34
|
+
| WorkerLogMessage;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parent-side view of a worker subprocess: send typed inbound messages,
|
|
38
|
+
* subscribe to outbound messages and worker errors, and hard-terminate.
|
|
39
|
+
*/
|
|
40
|
+
export interface WorkerHandle<Inbound, Outbound> {
|
|
41
|
+
send(message: Inbound): void;
|
|
42
|
+
onMessage(handler: (message: Outbound) => void): () => void;
|
|
43
|
+
onError(handler: (error: Error) => void): () => void;
|
|
44
|
+
terminate(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A {@link WorkerHandle} that can also be (un)referenced so a pending request
|
|
49
|
+
* keeps the parent event loop alive while an idle worker never blocks exit.
|
|
50
|
+
*/
|
|
51
|
+
export interface RefCountedWorkerHandle<Inbound, Outbound> extends WorkerHandle<Inbound, Outbound> {
|
|
52
|
+
/** Re-reference the subprocess so a pending request keeps the parent event loop alive. */
|
|
53
|
+
ref(): void;
|
|
54
|
+
/** Drop the reference once the worker is idle so it never blocks process exit. */
|
|
55
|
+
unref(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The raw spawned subprocess plus the parent-side fan-out sets. */
|
|
59
|
+
export interface SpawnedSubprocess<Outbound> {
|
|
60
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
61
|
+
inbound: Set<(message: Outbound) => void>;
|
|
62
|
+
errors: Set<(error: Error) => void>;
|
|
63
|
+
/**
|
|
64
|
+
* Flipped to `true` right before the deliberate SIGKILL so `onExit` can
|
|
65
|
+
* distinguish the expected hard-kill from a crash (SIGSEGV from a native
|
|
66
|
+
* fault, OOM SIGKILL, operator `kill -9`). Only the latter surfaces as a
|
|
67
|
+
* worker error so callers don't await forever.
|
|
68
|
+
*/
|
|
69
|
+
intentionalExit: { value: boolean };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface WorkerSpawnCommand {
|
|
73
|
+
cmd: string[];
|
|
74
|
+
cwd?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Cold-starting a worker from a compiled binary (decompress + module graph
|
|
79
|
+
* load) is slow on contended CI runners; the probe only proves the worker
|
|
80
|
+
* spawns and ponges, so a generous bound removes flakes without weakening it.
|
|
81
|
+
*/
|
|
82
|
+
export const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the command used to relaunch the agent CLI into worker mode. In a
|
|
86
|
+
* compiled binary the entry point is the binary itself; otherwise re-enter the
|
|
87
|
+
* declared worker-host entry with a cwd-relative script path (Bun's subprocess
|
|
88
|
+
* IPC is more reliable that way under `bun test`), falling back to this
|
|
89
|
+
* package's own `src/cli.ts` when no host entry is declared (bun test, SDK
|
|
90
|
+
* embedding).
|
|
91
|
+
*/
|
|
92
|
+
export function resolveWorkerSpawnCmd(workerArg: string): WorkerSpawnCommand {
|
|
93
|
+
if (isCompiledBinary()) return { cmd: [process.execPath, workerArg] };
|
|
94
|
+
const hostEntry = workerHostEntry();
|
|
95
|
+
if (hostEntry) {
|
|
96
|
+
return { cmd: [process.execPath, path.basename(hostEntry), workerArg], cwd: path.dirname(hostEntry) };
|
|
97
|
+
}
|
|
98
|
+
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
99
|
+
return { cmd: [process.execPath, "src/cli.ts", workerArg], cwd: packageRoot };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Snapshot the parent environment for the child. `process.env` carries
|
|
104
|
+
* `undefined` slots that `Bun.spawn` rejects, so filter them out; an optional
|
|
105
|
+
* `overlay` (e.g. the tiny-model device/dtype vars) wins over inherited keys.
|
|
106
|
+
*/
|
|
107
|
+
export function workerEnvFromParent(overlay?: Record<string, string>): Record<string, string> {
|
|
108
|
+
const base = $env as Record<string, string | undefined>;
|
|
109
|
+
const merged: Record<string, string> = {};
|
|
110
|
+
for (const key in base) {
|
|
111
|
+
const value = base[key];
|
|
112
|
+
if (typeof value === "string") merged[key] = value;
|
|
113
|
+
}
|
|
114
|
+
if (overlay) {
|
|
115
|
+
for (const key in overlay) merged[key] = overlay[key];
|
|
116
|
+
}
|
|
117
|
+
return merged;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Spawn an inference worker subprocess and wire its IPC fan-out. The child
|
|
122
|
+
* inherits no stdio (native model runtimes may otherwise print progress or
|
|
123
|
+
* decoded text and corrupt the chat scrollback) and is `unref`'d outside `bun
|
|
124
|
+
* test` so an idle worker never blocks process exit. `exitLabel` prefixes the
|
|
125
|
+
* worker-error message surfaced for an unexpected (non-intentional) exit.
|
|
126
|
+
*/
|
|
127
|
+
export function createWorkerSubprocess<Outbound>(options: {
|
|
128
|
+
spawnCommand: WorkerSpawnCommand;
|
|
129
|
+
env: Record<string, string>;
|
|
130
|
+
exitLabel: string;
|
|
131
|
+
}): SpawnedSubprocess<Outbound> {
|
|
132
|
+
const inbound = new Set<(message: Outbound) => void>();
|
|
133
|
+
const errors = new Set<(error: Error) => void>();
|
|
134
|
+
const intentionalExit = { value: false };
|
|
135
|
+
const proc = Bun.spawn({
|
|
136
|
+
cmd: options.spawnCommand.cmd,
|
|
137
|
+
cwd: options.spawnCommand.cwd,
|
|
138
|
+
env: options.env,
|
|
139
|
+
stdin: "ignore",
|
|
140
|
+
stdout: "ignore",
|
|
141
|
+
stderr: "ignore",
|
|
142
|
+
serialization: "advanced",
|
|
143
|
+
windowsHide: true,
|
|
144
|
+
ipc(message) {
|
|
145
|
+
for (const handler of inbound) handler(message as Outbound);
|
|
146
|
+
},
|
|
147
|
+
onExit(_proc, exitCode, signalCode) {
|
|
148
|
+
if (exitCode === 0) return;
|
|
149
|
+
// Swallow only the expected SIGKILL from `terminate()`; every other
|
|
150
|
+
// signal exit (SIGSEGV from a native fault, OOM SIGKILL, operator
|
|
151
|
+
// `kill -9`) is a real worker death that must fault in-flight
|
|
152
|
+
// requests so callers don't await forever.
|
|
153
|
+
if (exitCode === null && intentionalExit.value) return;
|
|
154
|
+
const reason = exitCode !== null ? `code ${exitCode}` : `signal ${signalCode ?? "unknown"}`;
|
|
155
|
+
const err = new Error(`${options.exitLabel} exited with ${reason}`);
|
|
156
|
+
for (const handler of errors) handler(err);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
// Don't keep the parent event loop alive on an idle worker; the dispose
|
|
160
|
+
// path calls `terminate()` explicitly. Bun's test runner starves IPC for
|
|
161
|
+
// unref'd subprocesses, so keep it referenced only under tests.
|
|
162
|
+
if (!isBunTestRuntime()) proc.unref();
|
|
163
|
+
return { proc, inbound, errors, intentionalExit };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Wrap a {@link SpawnedSubprocess} as a {@link WorkerHandle}. The `send`
|
|
168
|
+
* strategy is injected so each client keeps its exact IPC-send behaviour (e.g.
|
|
169
|
+
* `safeSend` vs an inline guarded `proc.send`). `terminate()` SIGKILLs: the
|
|
170
|
+
* point of subprocess isolation is that the parent never runs
|
|
171
|
+
* `onnxruntime-node`'s NAPI finalizer (it crashes Bun on Windows), so the OS
|
|
172
|
+
* reclaims the model memory instead. The intentional-exit flag is flipped
|
|
173
|
+
* *before* the kill so `onExit` can tell it apart from a native crash.
|
|
174
|
+
*/
|
|
175
|
+
export function createWorkerHandle<Inbound, Outbound>(
|
|
176
|
+
spawned: SpawnedSubprocess<Outbound>,
|
|
177
|
+
send: (message: Inbound) => void,
|
|
178
|
+
): WorkerHandle<Inbound, Outbound> {
|
|
179
|
+
const { proc, inbound, errors, intentionalExit } = spawned;
|
|
180
|
+
return {
|
|
181
|
+
send,
|
|
182
|
+
onMessage(handler) {
|
|
183
|
+
inbound.add(handler);
|
|
184
|
+
return () => inbound.delete(handler);
|
|
185
|
+
},
|
|
186
|
+
onError(handler) {
|
|
187
|
+
errors.add(handler);
|
|
188
|
+
return () => errors.delete(handler);
|
|
189
|
+
},
|
|
190
|
+
async terminate() {
|
|
191
|
+
intentionalExit.value = true;
|
|
192
|
+
try {
|
|
193
|
+
proc.kill("SIGKILL");
|
|
194
|
+
} catch {
|
|
195
|
+
// Already gone.
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A stand-in handle used when the worker subprocess cannot be spawned. It
|
|
203
|
+
* ponges `ping` (so the smoke probe and readiness checks still resolve) and
|
|
204
|
+
* answers every other request with the spawn error so callers fail fast
|
|
205
|
+
* instead of awaiting forever.
|
|
206
|
+
*/
|
|
207
|
+
export function createUnavailableWorker<
|
|
208
|
+
Inbound extends { type: string; id: string },
|
|
209
|
+
Outbound extends { type: string },
|
|
210
|
+
>(error: unknown): WorkerHandle<Inbound, Outbound> {
|
|
211
|
+
const listeners = new Set<(message: Outbound) => void>();
|
|
212
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
213
|
+
const emit = (message: WorkerOutboundBase): void => {
|
|
214
|
+
// The stub only ever emits pong/error — members of every concrete worker
|
|
215
|
+
// Outbound union — but the generic cannot prove it, hence the assertion.
|
|
216
|
+
for (const listener of listeners) listener(message as unknown as Outbound);
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
send(message) {
|
|
220
|
+
queueMicrotask(() => {
|
|
221
|
+
if (message.type === "ping") {
|
|
222
|
+
emit({ type: "pong", id: message.id });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
emit({ type: "error", id: message.id, error: errorMessage });
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
onMessage(handler) {
|
|
229
|
+
listeners.add(handler);
|
|
230
|
+
return () => listeners.delete(handler);
|
|
231
|
+
},
|
|
232
|
+
onError() {
|
|
233
|
+
return () => {};
|
|
234
|
+
},
|
|
235
|
+
async terminate() {
|
|
236
|
+
listeners.clear();
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Spawn a worker handle, falling back to {@link createUnavailableWorker} (after
|
|
243
|
+
* a warning) when the subprocess cannot be created so the feature degrades
|
|
244
|
+
* gracefully instead of throwing into callers.
|
|
245
|
+
*/
|
|
246
|
+
export function spawnWorkerOrUnavailable<Handle>(
|
|
247
|
+
spawn: () => Handle,
|
|
248
|
+
unavailable: (error: unknown) => Handle,
|
|
249
|
+
warnMessage: string,
|
|
250
|
+
): Handle {
|
|
251
|
+
try {
|
|
252
|
+
return spawn();
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.warn(warnMessage, { error: error instanceof Error ? error.message : String(error) });
|
|
255
|
+
return unavailable(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Forward a worker's structured `log` message to the matching logger level. */
|
|
260
|
+
export function logWorkerMessage(message: WorkerLogMessage): void {
|
|
261
|
+
if (message.level === "debug") logger.debug(message.msg, message.meta);
|
|
262
|
+
else if (message.level === "warn") logger.warn(message.msg, message.meta);
|
|
263
|
+
else logger.error(message.msg, message.meta);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Drive the ping/pong readiness probe wired into `omp --smoke-test`: send one
|
|
268
|
+
* `ping`, resolve on the first `pong` (ignoring `log` chatter), and reject on
|
|
269
|
+
* any other message, a worker error, or the timeout. Always tears the handle
|
|
270
|
+
* down on the way out. `label` prefixes the failure messages.
|
|
271
|
+
*/
|
|
272
|
+
export async function smokeTestWorker<Inbound extends { type: string; id: string }, Outbound extends { type: string }>(
|
|
273
|
+
handle: WorkerHandle<Inbound, Outbound>,
|
|
274
|
+
label: string,
|
|
275
|
+
timeoutMs: number,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
278
|
+
const timer = setTimeout(() => reject(new Error(`${label} did not pong within ${timeoutMs}ms`)), timeoutMs);
|
|
279
|
+
const unsubscribeMessage = handle.onMessage(message => {
|
|
280
|
+
if (message.type === "pong") {
|
|
281
|
+
resolve();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (message.type === "log") return;
|
|
285
|
+
reject(new Error(`${label}: expected pong, got ${JSON.stringify(message)}`));
|
|
286
|
+
});
|
|
287
|
+
const unsubscribeError = handle.onError(reject);
|
|
288
|
+
try {
|
|
289
|
+
handle.send({ type: "ping", id: "smoke" } as Inbound);
|
|
290
|
+
await promise;
|
|
291
|
+
} finally {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
unsubscribeMessage();
|
|
294
|
+
unsubscribeError();
|
|
295
|
+
await handle.terminate();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ProgressInfo } from "@huggingface/transformers";
|
|
4
|
+
import {
|
|
5
|
+
ensureRuntimeInstalled,
|
|
6
|
+
getTinyModelsCacheDir,
|
|
7
|
+
installRuntimeModuleResolver,
|
|
8
|
+
isCompiledBinary,
|
|
9
|
+
resolveRuntimeModule,
|
|
10
|
+
} from "@oh-my-pi/pi-utils";
|
|
11
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Child-side scaffolding shared by the ONNX inference worker bodies
|
|
15
|
+
* (`stt/asr-worker`, `tiny/worker`, `tts/tts-worker`). These are the helpers
|
|
16
|
+
* that run inside the spawned subprocess: error serialization, structured log
|
|
17
|
+
* and progress reporting over the worker's typed transport, side-runtime
|
|
18
|
+
* install (sharp stubbing + module-resolver patch), once-per-process runtime
|
|
19
|
+
* memoization, and the Transformers.js runtime loader. The parent/client-side
|
|
20
|
+
* complement lives in `worker-client.ts`.
|
|
21
|
+
*
|
|
22
|
+
* Each worker keeps its own strongly-typed transport / model-key / progress
|
|
23
|
+
* event; the structural {@link WorkerLogTransport} / {@link WorkerProgressTransport}
|
|
24
|
+
* interfaces below are the minimal shapes these helpers need, and every worker's
|
|
25
|
+
* concrete transport satisfies them.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
|
|
29
|
+
const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
|
|
30
|
+
const sourceRequire = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
// ── Error serialization ─────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export function errorText(error: unknown): string {
|
|
35
|
+
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function errorMessage(error: unknown): string {
|
|
39
|
+
return error instanceof Error ? error.message : String(error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Structured logging ──────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export type WorkerLogLevel = "debug" | "warn" | "error";
|
|
45
|
+
|
|
46
|
+
/** Minimal transport surface a worker exposes for forwarding log lines. */
|
|
47
|
+
export interface WorkerLogTransport {
|
|
48
|
+
send(message: { type: "log"; level: WorkerLogLevel; msg: string; meta?: Record<string, unknown> }): void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function sendLog(
|
|
52
|
+
transport: WorkerLogTransport,
|
|
53
|
+
level: WorkerLogLevel,
|
|
54
|
+
msg: string,
|
|
55
|
+
meta?: Record<string, unknown>,
|
|
56
|
+
): void {
|
|
57
|
+
transport.send({ type: "log", level, msg, meta });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Progress reporting ──────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generic worker progress event. Each worker's protocol declares an identical
|
|
64
|
+
* shape with its own `modelKey` type; this is the parameterized version the
|
|
65
|
+
* shared helpers emit, structurally assignable to each protocol's event.
|
|
66
|
+
*/
|
|
67
|
+
export interface WorkerProgressEvent<K> {
|
|
68
|
+
modelKey: K;
|
|
69
|
+
status: "initiate" | "download" | "progress" | "progress_total" | "done" | "ready" | "error";
|
|
70
|
+
name?: string;
|
|
71
|
+
file?: string;
|
|
72
|
+
progress?: number;
|
|
73
|
+
loaded?: number;
|
|
74
|
+
total?: number;
|
|
75
|
+
files?: Record<string, { loaded: number; total: number }>;
|
|
76
|
+
task?: string;
|
|
77
|
+
model?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Minimal transport surface a worker exposes for emitting progress events. */
|
|
81
|
+
export interface WorkerProgressTransport<K> {
|
|
82
|
+
send(message: { type: "progress"; id: string; event: WorkerProgressEvent<K> }): void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Map a Transformers.js {@link ProgressInfo} onto the worker progress event. */
|
|
86
|
+
function toProgressEvent<K>(modelKey: K, info: ProgressInfo): WorkerProgressEvent<K> {
|
|
87
|
+
if (info.status === "ready") {
|
|
88
|
+
return { modelKey, status: info.status, task: info.task, model: info.model };
|
|
89
|
+
}
|
|
90
|
+
if (info.status === "progress_total") {
|
|
91
|
+
return {
|
|
92
|
+
modelKey,
|
|
93
|
+
status: info.status,
|
|
94
|
+
name: info.name,
|
|
95
|
+
progress: info.progress,
|
|
96
|
+
loaded: info.loaded,
|
|
97
|
+
total: info.total,
|
|
98
|
+
files: info.files,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (info.status === "progress") {
|
|
102
|
+
return {
|
|
103
|
+
modelKey,
|
|
104
|
+
status: info.status,
|
|
105
|
+
name: info.name,
|
|
106
|
+
file: info.file,
|
|
107
|
+
progress: info.progress,
|
|
108
|
+
loaded: info.loaded,
|
|
109
|
+
total: info.total,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return { modelKey, status: info.status, name: info.name, file: info.file };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function sendProgress<K>(
|
|
116
|
+
transport: WorkerProgressTransport<K>,
|
|
117
|
+
id: string,
|
|
118
|
+
modelKey: K,
|
|
119
|
+
info: ProgressInfo,
|
|
120
|
+
): void {
|
|
121
|
+
transport.send({ type: "progress", id, event: toProgressEvent(modelKey, info) });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Model cache ─────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* If a model is already warming/warm in `cache`, replay a `ready` progress
|
|
128
|
+
* event for this request once it resolves and return the cached promise so the
|
|
129
|
+
* caller can short-circuit; otherwise return `undefined`.
|
|
130
|
+
*/
|
|
131
|
+
export function replayCachedReady<K, M>(
|
|
132
|
+
cache: Map<K, Promise<M>>,
|
|
133
|
+
modelKey: K,
|
|
134
|
+
transport: WorkerProgressTransport<K>,
|
|
135
|
+
requestId: string,
|
|
136
|
+
task: string,
|
|
137
|
+
model: string,
|
|
138
|
+
): Promise<M> | undefined {
|
|
139
|
+
const cached = cache.get(modelKey);
|
|
140
|
+
if (!cached) return undefined;
|
|
141
|
+
void cached
|
|
142
|
+
.then(() => {
|
|
143
|
+
transport.send({ type: "progress", id: requestId, event: { modelKey, status: "ready", task, model } });
|
|
144
|
+
})
|
|
145
|
+
.catch(() => undefined);
|
|
146
|
+
return cached;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Side-runtime install scaffolding ────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stub `sharp` (the speech/text pipelines are not image codecs, so the native
|
|
153
|
+
* image dependency is dead weight) and patch the module resolver so a side
|
|
154
|
+
* runtime's bare requires resolve against its own `node_modules`. Returns the
|
|
155
|
+
* runtime's `node_modules` directory.
|
|
156
|
+
*/
|
|
157
|
+
export async function installSharpStubResolver(runtimeDir: string): Promise<string> {
|
|
158
|
+
const nodeModules = path.join(runtimeDir, "node_modules");
|
|
159
|
+
const sharpStub = path.join(runtimeDir, "omp-sharp-stub.cjs");
|
|
160
|
+
await Bun.write(sharpStub, "module.exports = {};\n");
|
|
161
|
+
installRuntimeModuleResolver({ runtimeNodeModules: nodeModules, stubs: { sharp: sharpStub } });
|
|
162
|
+
return nodeModules;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Prepare a freshly-installed compiled runtime for loading and return the
|
|
167
|
+
* absolute entrypoint of `packageName` to `require`.
|
|
168
|
+
*/
|
|
169
|
+
async function prepareCompiledRuntime(runtimeDir: string, packageName: string): Promise<string> {
|
|
170
|
+
const nodeModules = await installSharpStubResolver(runtimeDir);
|
|
171
|
+
const entry = resolveRuntimeModule(nodeModules, packageName);
|
|
172
|
+
if (!entry) throw new Error(`Unable to resolve ${packageName} in compiled runtime at ${nodeModules}`);
|
|
173
|
+
return entry;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Transformers version resolution ─────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function resolveTransformersVersionSpec(): string {
|
|
179
|
+
const manifest = packageJson as {
|
|
180
|
+
optionalDependencies?: Record<string, string>;
|
|
181
|
+
dependencies?: Record<string, string>;
|
|
182
|
+
};
|
|
183
|
+
const versionSpec =
|
|
184
|
+
manifest.optionalDependencies?.[TRANSFORMERS_PACKAGE] ?? manifest.dependencies?.[TRANSFORMERS_PACKAGE];
|
|
185
|
+
if (!versionSpec) throw new Error(`${TRANSFORMERS_PACKAGE} is missing from package.json optionalDependencies`);
|
|
186
|
+
if (!versionSpec.startsWith("catalog:")) return versionSpec;
|
|
187
|
+
if (COMPILED_TRANSFORMERS_VERSION) return COMPILED_TRANSFORMERS_VERSION;
|
|
188
|
+
const installed = sourceRequire(`${TRANSFORMERS_PACKAGE}/package.json`) as { version: string };
|
|
189
|
+
return installed.version;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let cachedTransformersVersionSpec: string | undefined;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Lazily resolve (and memoize) the transformers version spec. In the `catalog:`
|
|
196
|
+
* case {@link resolveTransformersVersionSpec} `require`s the installed
|
|
197
|
+
* `@huggingface/transformers/package.json`, so it is only ever touched on the
|
|
198
|
+
* compiled-binary runtime-install path — loading a worker (smoke-test ping,
|
|
199
|
+
* online path) never triggers the transformers resolve/install dance.
|
|
200
|
+
*/
|
|
201
|
+
export function getTransformersVersionSpec(): string {
|
|
202
|
+
cachedTransformersVersionSpec ??= resolveTransformersVersionSpec();
|
|
203
|
+
return cachedTransformersVersionSpec;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Transformers runtime loader ─────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/** The subset of the Transformers.js module surface {@link configureTransformers} touches. */
|
|
209
|
+
interface ConfigurableTransformers {
|
|
210
|
+
env: { cacheDir?: string; allowLocalModels?: boolean; logLevel?: unknown };
|
|
211
|
+
LogLevel: { ERROR: unknown };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function configureTransformers<T extends ConfigurableTransformers>(transformers: T): T {
|
|
215
|
+
transformers.env.cacheDir = getTinyModelsCacheDir();
|
|
216
|
+
transformers.env.allowLocalModels = false;
|
|
217
|
+
transformers.env.logLevel = transformers.LogLevel.ERROR;
|
|
218
|
+
return transformers;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Memoize an async runtime load so it runs at most once per process, clearing
|
|
223
|
+
* the cache on failure so a later call can retry. Each worker holds one
|
|
224
|
+
* instance per runtime it loads.
|
|
225
|
+
*/
|
|
226
|
+
export class MemoizedRuntime<T> {
|
|
227
|
+
#promise: Promise<T> | null = null;
|
|
228
|
+
|
|
229
|
+
load(build: () => Promise<T>): Promise<T> {
|
|
230
|
+
if (this.#promise) return this.#promise;
|
|
231
|
+
const promise = build().catch(error => {
|
|
232
|
+
this.#promise = null;
|
|
233
|
+
throw error;
|
|
234
|
+
});
|
|
235
|
+
this.#promise = promise;
|
|
236
|
+
return promise;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Load the `@huggingface/transformers` runtime into `holder` (memoized): from
|
|
242
|
+
* the ambient install when running from source, or from a version-keyed side
|
|
243
|
+
* runtime (resolved lazily at `runtimeDir()`) when running as a compiled binary.
|
|
244
|
+
* The result is cast to the caller's concrete runtime type `T`.
|
|
245
|
+
*/
|
|
246
|
+
export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
|
|
247
|
+
holder: MemoizedRuntime<T>,
|
|
248
|
+
transport: WorkerProgressTransport<K>,
|
|
249
|
+
requestId: string,
|
|
250
|
+
modelKey: K,
|
|
251
|
+
runtimeDir: () => string,
|
|
252
|
+
): Promise<T> {
|
|
253
|
+
return holder.load(async () => {
|
|
254
|
+
if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as T);
|
|
255
|
+
const installedDir = await ensureRuntimeInstalled({
|
|
256
|
+
runtimeDir: runtimeDir(),
|
|
257
|
+
install: {
|
|
258
|
+
dependencies: { [TRANSFORMERS_PACKAGE]: getTransformersVersionSpec() },
|
|
259
|
+
trustedDependencies: ["onnxruntime-node"],
|
|
260
|
+
},
|
|
261
|
+
probePackage: TRANSFORMERS_PACKAGE,
|
|
262
|
+
onPhase: phase =>
|
|
263
|
+
transport.send({
|
|
264
|
+
type: "progress",
|
|
265
|
+
id: requestId,
|
|
266
|
+
event: {
|
|
267
|
+
modelKey,
|
|
268
|
+
status: phase,
|
|
269
|
+
name: `${TRANSFORMERS_PACKAGE}@${getTransformersVersionSpec()}`,
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
});
|
|
273
|
+
const entry = await prepareCompiledRuntime(installedDir, TRANSFORMERS_PACKAGE);
|
|
274
|
+
const require_ = createRequire(entry);
|
|
275
|
+
return configureTransformers(require_(entry) as T);
|
|
276
|
+
});
|
|
277
|
+
}
|
package/src/task/executor.ts
CHANGED
|
@@ -41,7 +41,8 @@ import type { AuthStorage } from "../session/auth-storage";
|
|
|
41
41
|
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
42
42
|
import { SessionManager } from "../session/session-manager";
|
|
43
43
|
import { truncateTail } from "../session/streaming-output";
|
|
44
|
-
import type { ContextFileEntry } from "../tools";
|
|
44
|
+
import type { ContextFileEntry, ToolSession } from "../tools";
|
|
45
|
+
import { resolveEvalBackends } from "../tools/eval-backends";
|
|
45
46
|
import { isIrcEnabled } from "../tools/irc";
|
|
46
47
|
import { normalizeSchema } from "../tools/jtd-to-json-schema";
|
|
47
48
|
import {
|
|
@@ -1790,10 +1791,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1790
1791
|
toolNames = [...toolNames, "irc"];
|
|
1791
1792
|
}
|
|
1792
1793
|
if (toolNames?.includes("exec")) {
|
|
1793
|
-
const
|
|
1794
|
-
const allowEvalJs = settings.get("eval.js") ?? true;
|
|
1794
|
+
const backends = resolveEvalBackends({ settings } as ToolSession);
|
|
1795
1795
|
const expanded = toolNames.filter(name => name !== "exec");
|
|
1796
|
-
if (
|
|
1796
|
+
if (backends.python || backends.js || backends.ruby || backends.julia) expanded.push("eval");
|
|
1797
1797
|
expanded.push("bash");
|
|
1798
1798
|
toolNames = Array.from(new Set(expanded));
|
|
1799
1799
|
}
|