@qvac/harness 0.5.0 → 0.7.0

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 (68) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/README.md +78 -27
  3. package/dist-lib/index.d.ts +15 -1
  4. package/dist-lib/index.js +175 -53
  5. package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
  6. package/dist-lib/lib/connect.d.ts +3 -1
  7. package/dist-lib/lib/connect.js +23 -2
  8. package/dist-lib/lib/contract-types.d.ts +14 -0
  9. package/dist-lib/lib/docx/index.d.ts +1 -1
  10. package/dist-lib/lib/docx/index.js +1 -1
  11. package/dist-lib/lib/hardware/capability-probe.js +9 -4
  12. package/dist-lib/lib/hooks/built-in/sandbox.js +0 -5
  13. package/dist-lib/lib/models/model-catalog-constants.d.ts +7 -0
  14. package/dist-lib/lib/models/model-catalog-constants.js +17 -0
  15. package/dist-lib/lib/models/model-catalog.d.ts +16 -6
  16. package/dist-lib/lib/models/model-catalog.js +21 -6
  17. package/dist-lib/lib/models/model-list.d.ts +2 -0
  18. package/dist-lib/lib/models/model-list.js +9 -4
  19. package/dist-lib/lib/models/model-pool.d.ts +6 -2
  20. package/dist-lib/lib/models/model-pool.js +67 -11
  21. package/dist-lib/lib/models/model-task.d.ts +3 -0
  22. package/dist-lib/lib/models/model-task.js +14 -0
  23. package/dist-lib/lib/models/music-catalog.d.ts +2 -0
  24. package/dist-lib/lib/models/music-catalog.js +9 -0
  25. package/dist-lib/lib/office/index.d.ts +1 -1
  26. package/dist-lib/lib/office/index.js +1 -1
  27. package/dist-lib/lib/office/python.js +1 -2
  28. package/dist-lib/lib/serve.js +12 -0
  29. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
  30. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
  31. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
  32. package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
  33. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
  34. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
  35. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
  36. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
  37. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
  38. package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
  39. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
  40. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
  45. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
  46. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
  47. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
  48. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
  49. package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
  50. package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
  51. package/dist-lib/lib/tools/exec.d.ts +0 -2
  52. package/dist-lib/lib/tools/exec.js +6 -11
  53. package/dist-lib/schema/harness.js +27 -1
  54. package/dist-lib/spec/bind.js +6 -0
  55. package/dist-lib/spec/calls.js +2 -0
  56. package/dist-lib/spec/hrpc/hrpc.json +26 -0
  57. package/dist-lib/spec/hrpc/index.js +27 -3
  58. package/dist-lib/spec/hrpc/messages.js +72 -5
  59. package/dist-lib/spec/hyperschema/index.js +72 -5
  60. package/dist-lib/spec/hyperschema/schema.json +53 -1
  61. package/dist-lib/spec/hyperschema/types.d.ts +18 -1
  62. package/package.json +40 -20
  63. package/dist/harness.bundle +0 -42312
  64. package/dist/harness.cjs +0 -18
  65. package/dist/harness.d.ts +0 -8
  66. package/dist/prebuilds-manifest.json +0 -372
  67. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
  68. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
@@ -1,8 +1,8 @@
1
1
  import { AUDIOGEN_ACESTEP_5HZ_LM_0_6B_Q8_0, AUDIOGEN_ACESTEP_V15_SFT_Q8_0, AUDIOGEN_ACESTEP_V15_TURBO_Q4_K_M, AUDIOGEN_ACESTEP_V15_TURBO_Q8_0, AUDIOGEN_QWEN3_EMBEDDING_0_6B_Q8_0, AUDIOGEN_VAE_BF16, EMBEDDINGGEMMA_300M_Q4_0, FLUX_2_KLEIN_4B_Q4_0, FLUX_2_KLEIN_4B_VAE, HEALTHCARE_1_7B_MEDICAL_Q4_K_M, HEALTHCARE_4B_MEDICAL_Q4_K_M, MMPROJ_QWEN3_5_2B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_4B_MULTIMODAL_Q8_0, MMPROJ_QWEN3_5_9B_MULTIMODAL_F16, MMPROJ_QWEN3_6_27B_MULTIMODAL_F16, MMPROJ_QWEN3_6_35B_A3B_MULTIMODAL_F16, MMPROJ_QWEN3_8_27B_MULTIMODAL_F16, QWEN3_5_2B_MULTIMODAL_Q4_K_M, QWEN3_5_4B_MULTIMODAL_Q4_K_M, QWEN3_5_9B_MULTIMODAL_Q4_K_M, PARAKEET_TDT_0_6B_V3_Q8_0, QWEN3_4B_Q4_K_M, QWEN3_6_27B_MULTIMODAL_Q4_K_XL, QWEN3_6_35B_A3B_MULTIMODAL_Q4_K_M, QWEN3_8_27B_MULTIMODAL_UD_Q4_K_XL, SD_V2_1_1B_Q8_0, SDXL_BASE_1_0_3B_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q4_0, TTS_MULTILINGUAL_SUPERTONIC3_Q8_0 } from '@qvac/sdk/models';
2
2
  import { detectToolDialect } from './tool-dialect.js';
3
3
  import { defaultTtsModelName } from './tts-model-default.js';
4
- import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG } from './model-catalog-constants.js';
5
- export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG };
4
+ import { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName } from './model-catalog-constants.js';
5
+ export { CAPABILITY_PROFILE_ORDER, QWEN_35_2B, QWEN_35_4B, QWEN_35_9B, QWEN_36_27B, QWEN_36_35B_A3B, QWEN_38_27B, MEDPSY_1_7B, MEDPSY_4B, isCapabilityProfile, meetsMinCapabilityProfile, modelNameToCapabilityProfile, capabilityProfileToModelName, isMultimodalLlm, AGENT_MODEL_NAMES, modelDisplayName, capabilityProfileModelLabel, DEFAULT_IMAGE_MODEL_CHOICE, IMAGE_CHOICE_CATALOG, isMobilePlatform, runnableModelName };
6
6
  export const LLM_CATALOG = [
7
7
  {
8
8
  name: QWEN_35_2B,
@@ -165,8 +165,8 @@ export function diffusionDownloadMetadata(model, addon) {
165
165
  assetSizes
166
166
  };
167
167
  }
168
- import { DEFAULT_MUSIC_MODEL_CHOICE } from './music-catalog.js';
169
- export { DEFAULT_MUSIC_MODEL_CHOICE };
168
+ import { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom } from './music-catalog.js';
169
+ export { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom };
170
170
  const MUSIC_GENERATION_SKILL = 'music-generation';
171
171
  export const AUDIOGEN_CATALOG = [
172
172
  {
@@ -251,18 +251,26 @@ export function skillModelsForAgent(agent) {
251
251
  const choice = agent.imageModelChoice ?? DEFAULT_IMAGE_MODEL_CHOICE;
252
252
  const entry = DIFFUSION_CATALOG.find((candidate) => candidate.choice === choice);
253
253
  if (entry) {
254
- models.push({ name: entry.name, addon: entry.model.addon });
254
+ models.push({ name: entry.name, addon: entry.model.addon, use: 'image' });
255
255
  }
256
256
  }
257
257
  if (agentHasSkill(agent, MUSIC_GENERATION_SKILL)) {
258
258
  const choice = agent.musicModelChoice ?? DEFAULT_MUSIC_MODEL_CHOICE;
259
259
  const entry = AUDIOGEN_CATALOG.find((candidate) => candidate.choice === choice);
260
260
  if (entry) {
261
- models.push({ name: entry.name, addon: entry.dit.addon });
261
+ models.push({ name: entry.name, addon: entry.dit.addon, use: 'music' });
262
262
  }
263
263
  }
264
264
  return models;
265
265
  }
266
+ export function modelsForAgent(agent, host) {
267
+ const skills = skillModelsForAgent(agent);
268
+ if (!agent.modelName) {
269
+ return skills;
270
+ }
271
+ const name = runnableModelName(agent.modelName, host);
272
+ return [{ name, addon: 'llm', use: 'chat' }, ...skills];
273
+ }
266
274
  export function diffusionSizeBytes(entry) {
267
275
  return diffusionAssets(entry).reduce((sum, asset) => sum + asset.expectedSize, 0);
268
276
  }
@@ -332,6 +340,13 @@ export function curatedListables() {
332
340
  export function curatedModelAsset(name, addon) {
333
341
  return curatedListables().find((entry) => entry.asset.addon === addon && (entry.name === name || entry.asset.name === name))?.asset;
334
342
  }
343
+ export function modelAddonOf(model) {
344
+ const known = [...curatedListables(), ...musicListables()].find((entry) => entry.name === model || entry.asset.name === model || entry.asset.src === model);
345
+ return known?.asset.addon ?? 'llm';
346
+ }
347
+ export function residentSrc(model, addon) {
348
+ return curatedModelAsset(model, addon)?.src ?? model;
349
+ }
335
350
  export const CUSTOM_TASK_TYPES = ['llm'];
336
351
  let CUSTOM_MODELS = new Map();
337
352
  export function setCustomModels(entries) {
@@ -1,5 +1,6 @@
1
1
  import type * as QvacSdk from '@qvac/sdk';
2
2
  import type { ModelAddon, ModelRegistrySdk } from './model-source.ts';
3
+ import { type ModelTaskType } from './model-task.ts';
3
4
  export type ModelType = ModelAddon;
4
5
  export interface StaticCatalogEntry {
5
6
  readonly name: string;
@@ -19,6 +20,7 @@ export interface ModelInfo {
19
20
  expectedSize: number;
20
21
  actualSize?: number;
21
22
  label?: string;
23
+ taskType: ModelTaskType;
22
24
  }
23
25
  export interface ListModelsOptions {
24
26
  type?: ModelType;
@@ -1,3 +1,4 @@
1
+ import { addonTaskType } from './model-task.js';
1
2
  import { isCustomModelCached } from './custom-model-cache.js';
2
3
  import { companionRegistryPaths, curatedListables, customModels, musicListables } from './model-catalog.js';
3
4
  export const MODEL_STATUS_TTL_MS = 30_000;
@@ -42,7 +43,8 @@ async function musicRows(hydrate) {
42
43
  isCached: status.isCached && companionsCached,
43
44
  isLoaded: status.isLoaded,
44
45
  supportsImages: false,
45
- expectedSize: listable.expectedSize
46
+ expectedSize: listable.expectedSize,
47
+ taskType: addonTaskType('audiogen')
46
48
  });
47
49
  }
48
50
  return rows;
@@ -93,7 +95,8 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, {
93
95
  isLoaded: status.isLoaded,
94
96
  supportsImages: seesImages(listable.name, listable.asset.addon),
95
97
  expectedSize: status.expectedSize ?? listable.asset.expectedSize,
96
- actualSize: status.actualSize
98
+ actualSize: status.actualSize,
99
+ taskType: addonTaskType(listable.asset.addon)
97
100
  };
98
101
  });
99
102
  const music = !type || type === 'audiogen' ? await musicRows(hydrate) : [];
@@ -121,7 +124,8 @@ export async function listModels(sdk, { type, cachedOnly, fullRegistry } = {}, {
121
124
  isLoaded: status.isLoaded,
122
125
  supportsImages: seesImages(entry.name, entry.addon),
123
126
  expectedSize: status.expectedSize ?? entry.expectedSize,
124
- actualSize: status.actualSize
127
+ actualSize: status.actualSize,
128
+ taskType: addonTaskType(entry.addon)
125
129
  };
126
130
  });
127
131
  return cachedOnly ? rows.filter((row) => row.isCached) : rows;
@@ -139,7 +143,8 @@ export async function customModelRows({ type, cachedOnly }, { modelsDir, loaded
139
143
  isLoaded: loaded.has(entry.name),
140
144
  supportsImages: addon === 'llm' && Boolean(entry.projectionSrc),
141
145
  expectedSize: entry.expectedSize ?? 0,
142
- label: entry.label
146
+ label: entry.label,
147
+ taskType: addonTaskType(addon)
143
148
  };
144
149
  }));
145
150
  return cachedOnly ? rows.filter((row) => row.isCached) : rows;
@@ -75,6 +75,7 @@ declare class ModelPuddle extends ResourcePuddle<string, ModelHandle> {
75
75
  readonly resources: Map<string, PuddleEntry>;
76
76
  }
77
77
  type PoolLike = ModelPuddle;
78
+ export declare function modelOf(key: string): string;
78
79
  export declare function releaseModel(pool: PoolLike, key: string): void;
79
80
  export declare function loadedModels(pool: PoolLike): LoadedModel[];
80
81
  export interface ResidentLease {
@@ -86,6 +87,7 @@ export declare function leaseResidentModel(pool: PoolLike, wanted?: (model: stri
86
87
  export declare function isResident(pool: PoolLike, key: string): boolean;
87
88
  export declare function leaseCount(pool: PoolLike, key: string): number;
88
89
  export declare function evictIdle(pool: PoolLike): Promise<void>;
90
+ export declare function evictIdleWhere(pool: PoolLike, wanted: (key: string) => boolean): Promise<void>;
89
91
  export interface DeadlineOptions {
90
92
  deadlineMs: number;
91
93
  tickMs?: number;
@@ -95,9 +97,11 @@ export interface ModelCapacityOptions extends DeadlineOptions {
95
97
  signal?: {
96
98
  aborted?: boolean;
97
99
  } | undefined;
100
+ evictable?: (key: string) => boolean;
98
101
  }
99
- export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal }: ModelCapacityOptions): Promise<boolean>;
100
- export declare function evictModelWithin(pool: PoolLike, key: string, { deadlineMs, tickMs }: DeadlineOptions): Promise<boolean>;
102
+ export declare function awaitModelCapacity(pool: PoolLike, { cap, deadlineMs, tickMs, signal, evictable }: ModelCapacityOptions): Promise<boolean>;
103
+ export declare function evictModelWithin(pool: PoolLike, key: string, options: DeadlineOptions): Promise<boolean>;
104
+ export declare function evictModel(pool: PoolLike, model: string, options: DeadlineOptions): Promise<boolean>;
101
105
  export type LoadSdk = () => Promise<ModelLifecycleSdk>;
102
106
  export type EmbedLifecycleSdk = Pick<SdkModule, 'unloadModel' | 'modelRegistrySearch'> & Partial<CancelSdk> & {
103
107
  loadModel(params: {
@@ -69,6 +69,9 @@ class ModelPuddle extends ResourcePuddle {
69
69
  function entryOf(pool, key) {
70
70
  return pool.resources.get(key);
71
71
  }
72
+ export function modelOf(key) {
73
+ return key.split('\0')[0];
74
+ }
72
75
  export function releaseModel(pool, key) {
73
76
  if (pool.destroyed)
74
77
  return;
@@ -123,8 +126,43 @@ export async function evictIdle(pool) {
123
126
  await pool.gc();
124
127
  }
125
128
  }
126
- export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250, signal }) {
127
- const roomed = () => loadedModels(pool).length < cap;
129
+ export async function evictIdleWhere(pool, wanted) {
130
+ for (let tick = 0; tick <= pool.gcIdleTicks; tick++) {
131
+ if (pool.destroyed)
132
+ return;
133
+ const held = [];
134
+ for (const entry of pool.resources.values()) {
135
+ if (entry.refs === 0 && !wanted(entry.id)) {
136
+ held.push(entry.id);
137
+ }
138
+ }
139
+ if (pool.idle === held.length) {
140
+ return;
141
+ }
142
+ for (const key of held) {
143
+ pool.open(key);
144
+ }
145
+ try {
146
+ await pool.gc();
147
+ }
148
+ finally {
149
+ for (const key of held) {
150
+ releaseSettled(pool, key);
151
+ }
152
+ }
153
+ }
154
+ }
155
+ function residentCount(pool, wanted) {
156
+ let count = 0;
157
+ for (const entry of pool.resources.values()) {
158
+ if (wanted(entry.id) && entry.value.peek() !== null) {
159
+ count++;
160
+ }
161
+ }
162
+ return count;
163
+ }
164
+ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250, signal, evictable = () => true }) {
165
+ const roomed = () => residentCount(pool, evictable) < cap;
128
166
  let done = false;
129
167
  let timer = null;
130
168
  const expired = new Promise((resolve) => {
@@ -134,7 +172,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
134
172
  }, deadlineMs);
135
173
  });
136
174
  const evicting = (async () => {
137
- await evictIdle(pool);
175
+ await evictIdleWhere(pool, evictable);
138
176
  while (!roomed()) {
139
177
  if (done || signal?.aborted)
140
178
  return false;
@@ -142,7 +180,7 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
142
180
  if (done || signal?.aborted) {
143
181
  return false;
144
182
  }
145
- await evictIdle(pool);
183
+ await evictIdleWhere(pool, evictable);
146
184
  }
147
185
  return true;
148
186
  })();
@@ -156,18 +194,36 @@ export async function awaitModelCapacity(pool, { cap, deadlineMs, tickMs = 250,
156
194
  }
157
195
  }
158
196
  }
159
- export async function evictModelWithin(pool, key, { deadlineMs, tickMs = 250 }) {
197
+ export function evictModelWithin(pool, key, options) {
198
+ return evictWhereWithin(pool, (candidate) => candidate === key, options);
199
+ }
200
+ export function evictModel(pool, model, options) {
201
+ return evictWhereWithin(pool, (key) => modelOf(key) === model, options);
202
+ }
203
+ async function evictWhereWithin(pool, wanted, { deadlineMs, tickMs = 250 }) {
204
+ let done = false;
160
205
  const evicting = (async () => {
161
- while (isResident(pool, key)) {
162
- await evictIdle(pool);
163
- if (!isResident(pool, key)) {
206
+ while (residentCount(pool, wanted) > 0) {
207
+ await evictIdleWhere(pool, wanted);
208
+ if (residentCount(pool, wanted) === 0) {
164
209
  break;
165
210
  }
166
211
  await new Promise((resolve) => setTimeout(resolve, tickMs));
212
+ if (done) {
213
+ return false;
214
+ }
167
215
  }
168
216
  return true;
169
217
  })();
170
- return raceTimeout(evicting, { ms: deadlineMs, fallback: false });
218
+ try {
219
+ return await raceTimeout(evicting, { ms: deadlineMs, fallback: false });
220
+ }
221
+ finally {
222
+ done = true;
223
+ }
224
+ }
225
+ function definedFields(config) {
226
+ return Object.fromEntries(Object.entries(config).filter(([, value]) => value !== undefined));
171
227
  }
172
228
  function llmVerbosity() {
173
229
  const raw = Number(process.env.QVAC_LLM_VERBOSITY ?? 0);
@@ -253,7 +309,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
253
309
  const openclCache = deviceConfig.device !== 'cpu' && deviceConfig.openclCacheDir
254
310
  ? deviceConfig.openclCacheDir()
255
311
  : undefined;
256
- const modelConfig = {
312
+ const modelConfig = definedFields({
257
313
  verbosity: llmVerbosity(),
258
314
  tools: true,
259
315
  stop_sequences: RUNAWAY_STOP_SEQUENCES,
@@ -265,7 +321,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
265
321
  gpu_layers: deviceConfig.gpuLayers,
266
322
  'main-gpu': mainGpu,
267
323
  openclCacheDir: openclCache || undefined
268
- };
324
+ });
269
325
  const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
270
326
  hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
271
327
  const loadStarted = Date.now();
@@ -0,0 +1,3 @@
1
+ import type { ModelAddon } from './model-source.ts';
2
+ export type ModelTaskType = 'llm' | 'embeddings' | 'speech-to-text' | 'text-to-speech' | 'image-generation' | 'audio-generation' | 'translation' | 'text-recognition' | 'other';
3
+ export declare function addonTaskType(addon: ModelAddon): ModelTaskType;
@@ -0,0 +1,14 @@
1
+ const ADDON_TASK_TYPES = {
2
+ llm: 'llm',
3
+ embeddings: 'embeddings',
4
+ whisper: 'speech-to-text',
5
+ parakeet: 'speech-to-text',
6
+ tts: 'text-to-speech',
7
+ diffusion: 'image-generation',
8
+ audiogen: 'audio-generation',
9
+ nmt: 'translation',
10
+ ocr: 'text-recognition'
11
+ };
12
+ export function addonTaskType(addon) {
13
+ return ADDON_TASK_TYPES[addon] ?? 'other';
14
+ }
@@ -10,3 +10,5 @@ export interface MusicChoiceEntry {
10
10
  readonly notes: string;
11
11
  }
12
12
  export declare const MUSIC_CHOICE_CATALOG: ReadonlyArray<MusicChoiceEntry>;
13
+ export declare function musicModelChoiceFrom(settings: unknown): MusicModelChoice | undefined;
14
+ export declare function isMusicModelChoice(value: unknown): value is MusicModelChoice;
@@ -24,3 +24,12 @@ export const MUSIC_CHOICE_CATALOG = [
24
24
  notes: 'Fine-tuned quality. Wants a desktop GPU.'
25
25
  }
26
26
  ];
27
+ export function musicModelChoiceFrom(settings) {
28
+ if (!settings || typeof settings !== 'object' || !('modelChoice' in settings)) {
29
+ return undefined;
30
+ }
31
+ return isMusicModelChoice(settings.modelChoice) ? settings.modelChoice : undefined;
32
+ }
33
+ export function isMusicModelChoice(value) {
34
+ return MUSIC_MODEL_CHOICES.some((choice) => choice === value);
35
+ }
@@ -1,5 +1,5 @@
1
1
  import type { AbortSignal } from 'bare-abort-controller';
2
- export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/client.ts';
2
+ export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.ts';
3
3
  export interface OfficeExtractOptions {
4
4
  readonly signal?: AbortSignal | undefined;
5
5
  readonly mimeType?: string | undefined;
@@ -3,7 +3,7 @@ import { extractDocxText } from '../docx/index.js';
3
3
  import { extractWithPython } from './python.js';
4
4
  import { XLSX_EXTRACT } from './xlsx.js';
5
5
  import { PPTX_EXTRACT } from './pptx.js';
6
- export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/client.js';
6
+ export { closePythonWorker as closeOfficeWorker } from '../tools/exec/adapters/python/worker/active.js';
7
7
  const OFFICE_MIME_EXTENSIONS = {
8
8
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
9
9
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
@@ -10,8 +10,7 @@ export async function extractWithPython(bytes, fileName, { format, packages, sou
10
10
  const dir = wheelsDir();
11
11
  const manifest = vendoredManifest(dir);
12
12
  const install = planVendoredInstall(packages, { dir, manifest });
13
- if (!dir ||
14
- !manifest ||
13
+ if (!manifest ||
15
14
  !install ||
16
15
  !['micropip', ...install.packages].every((name) => Object.keys(manifest.files).some((file) => file.replaceAll('_', '-').startsWith(`${name}-`) && fs.existsSync(`${dir}/${file}`)))) {
17
16
  throw new Error(`bundled ${format} extraction packages are unavailable`);
@@ -191,6 +191,18 @@ export function serveHarness(stream, harness) {
191
191
  await harness.warmModel({ model, agent: agent ?? {} });
192
192
  return { ok: true };
193
193
  },
194
+ loadModel: async ({ model, agent }) => {
195
+ if (!harness.loadModel) {
196
+ throw new Error('harness does not support loadModel');
197
+ }
198
+ return harness.loadModel({ model, agent: agent ?? {} });
199
+ },
200
+ unloadModel: async ({ model }) => {
201
+ if (!harness.unloadModel) {
202
+ throw new Error('harness does not support unloadModel');
203
+ }
204
+ return harness.unloadModel({ model });
205
+ },
194
206
  modelEvents: () => modelEvents(harness),
195
207
  completion: completion(harness)
196
208
  });
@@ -0,0 +1,10 @@
1
+ export interface DeadlineData {
2
+ readonly state: SharedArrayBuffer;
3
+ readonly interrupt: SharedArrayBuffer;
4
+ readonly ms: number;
5
+ }
6
+ export declare const ARMED = 0;
7
+ export declare const DISARMED = 1;
8
+ export declare const FIRED = 2;
9
+ export declare const SIGINT = 2;
10
+ export declare const RESEND_MS = 100;
@@ -0,0 +1,5 @@
1
+ export const ARMED = 0;
2
+ export const DISARMED = 1;
3
+ export const FIRED = 2;
4
+ export const SIGINT = 2;
5
+ export const RESEND_MS = 100;
@@ -1,28 +1,21 @@
1
- import { spawnInlineThread } from '#pyodide-worker-spawn';
2
- const ARMED = 0;
3
- const DISARMED = 1;
4
- const FIRED = 2;
5
- const SIGINT = 2;
6
- const RESEND_MS = 100;
7
- const SOURCE = `
8
- const state = new Int32Array(data.state)
9
- const interrupt = new Uint8Array(data.interrupt)
10
- Atomics.wait(state, 0, ${ARMED}, data.ms)
11
- if (Atomics.compareExchange(state, 0, ${ARMED}, ${FIRED}) === ${ARMED}) {
12
- do Atomics.store(interrupt, 0, ${SIGINT})
13
- while (Atomics.wait(state, 0, ${FIRED}, ${RESEND_MS}) === 'timed-out')
14
- }
15
- `;
1
+ import { fileURLToPath } from 'url';
2
+ import { Worker } from 'worker_threads';
3
+ import { errorMessage } from '../../../../guards.js';
4
+ import { hlog } from '../../../../log.js';
5
+ import { DISARMED, FIRED } from './deadline-state.js';
16
6
  export function startInterruptTimer(ms) {
17
7
  const state = new Int32Array(new SharedArrayBuffer(4));
18
8
  const interrupt = new Uint8Array(new SharedArrayBuffer(1));
19
- const reap = spawnInlineThread(SOURCE, { state: state.buffer, interrupt: interrupt.buffer, ms });
9
+ const workerData = { state: state.buffer, interrupt: interrupt.buffer, ms };
10
+ const entry = fileURLToPath(import.meta.resolve('#pyodide-interrupt-deadline'));
11
+ new Worker(entry, { workerData })
12
+ .on('error', (error) => hlog('python: interrupt deadline failed', { err: errorMessage(error) }))
13
+ .unref();
20
14
  return {
21
15
  interrupt,
22
16
  disarm() {
23
17
  const fired = Atomics.exchange(state, 0, DISARMED) === FIRED;
24
18
  Atomics.notify(state, 0);
25
- reap();
26
19
  return fired;
27
20
  }
28
21
  };
@@ -1,14 +1,8 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
1
  import b4a from 'b4a';
10
2
  import { constants as fsConstants } from 'fs';
3
+ import path from 'path';
11
4
  import process from 'process';
5
+ import { fileURLToPath } from 'url';
12
6
  import { createGuardedFetch } from './guarded-fetch.js';
13
7
  import { seedPackageCache } from './wheels.js';
14
8
  class B4aTextDecoder {
@@ -95,15 +89,25 @@ export function pyodideLoadOptions() {
95
89
  }
96
90
  return { ...silentStdio, packageCacheDir: cacheDir };
97
91
  }
92
+ function pyodideIndexURL() {
93
+ import.meta.asset('pyodide/python_stdlib.zip');
94
+ import.meta.asset('pyodide/pyodide-lock.json');
95
+ return path.dirname(fileURLToPath(import.meta.asset('pyodide/pyodide.asm.wasm'))) + '/';
96
+ }
98
97
  export const defaultLoad = async () => {
99
98
  if (typeof globalThis.Bare !== 'undefined') {
100
99
  await shapeBareGlobals();
101
100
  }
102
- const moduleName = 'pyodide';
103
- const { loadPyodide } = await import(__rewriteRelativeImportExtension(moduleName));
101
+ const { loadPyodide } = await import('pyodide');
102
+ const { default: createPyodideModule } = await import('pyodide/pyodide.asm.mjs');
104
103
  const options = pyodideLoadOptions();
105
104
  if (options.packageCacheDir) {
106
105
  seedPackageCache(options.packageCacheDir);
107
106
  }
108
- return (await loadPyodide(options));
107
+ const indexURL = pyodideIndexURL();
108
+ return (await loadPyodide({
109
+ ...options,
110
+ indexURL,
111
+ createPyodideModule
112
+ }));
109
113
  };
@@ -1,12 +1,11 @@
1
1
  import { AbortController } from 'bare-abort-controller';
2
- import process from 'process';
3
2
  import ScopeLock from 'scope-lock';
4
3
  import { errorMessage } from '../../../../guards.js';
5
4
  import { AbortedError, raceAbort } from '../../../../helpers/race-abort.js';
6
5
  import { hlog } from '../../../../log.js';
7
6
  import { MAX_OUTPUT_CHARS } from '../../runner.js';
8
7
  import { startInterruptTimer } from './interrupt-timer.js';
9
- import { defaultLoad, pyodideLoadOptions } from './loader.js';
8
+ import { pyodideLoadOptions } from './loader.js';
10
9
  import { isPyodideFatal, performRun } from './run-session.js';
11
10
  import { runPythonInWorker } from './worker/client.js';
12
11
  export { pyodideLoadOptions };
@@ -98,11 +97,6 @@ async function runInProcess(load, spec, options) {
98
97
  stop.abort(undefined);
99
98
  }
100
99
  }
101
- function workerAvailable() {
102
- if (process.env.QVAC_PYODIDE_WORKER_ENTRY)
103
- return true;
104
- return !import.meta.url.includes('.bundle/');
105
- }
106
100
  export async function runPython(code, options = {}) {
107
101
  if (options.signal?.aborted)
108
102
  throw new AbortedError();
@@ -126,14 +120,6 @@ export async function runPython(code, options = {}) {
126
120
  askConnect: options.askConnect
127
121
  });
128
122
  }
129
- if (!workerAvailable()) {
130
- hlog('python: worker entry unavailable in this bundle; running in-process');
131
- return runInProcess(defaultLoad, spec, {
132
- timeoutMs,
133
- signal: options.signal,
134
- askConnect: options.askConnect
135
- });
136
- }
137
123
  await runLock.lock();
138
124
  try {
139
125
  if (options.signal?.aborted) {
@@ -12,6 +12,7 @@ export declare function pinned(spec: string): string;
12
12
  export declare function pipMessage(targets: readonly string[] | null, { sandbox }?: {
13
13
  sandbox?: boolean | undefined;
14
14
  }): string;
15
+ export declare function sandboxInstallError(command: string, declared: readonly string[] | undefined): string | null;
15
16
  export interface PipFromSourceHintOptions {
16
17
  readonly packages?: readonly string[] | undefined;
17
18
  readonly sandbox?: boolean;
@@ -242,6 +242,16 @@ export function pipMessage(targets, { sandbox = false } = {}) {
242
242
  const declare = specs ? `packages: ${JSON.stringify(specs)}` : 'the PyPI names in packages';
243
243
  return `pip is not available. Call exec with language "python", ${declare} and the Python source in command. Installing and running are the same one call.`;
244
244
  }
245
+ export function sandboxInstallError(command, declared) {
246
+ const install = pipInstallTargets(command)?.map(pinned) ?? null;
247
+ const packages = [...new Set([...(declared ?? []), ...(install ?? [])])];
248
+ if (packages.length === 0 || planVendoredInstall(packages)) {
249
+ return null;
250
+ }
251
+ return install
252
+ ? pipMessage(install, { sandbox: true })
253
+ : 'sandbox mode: installing packages reaches the network';
254
+ }
245
255
  const PIP_CALL = /\b(?:subprocess\.\w+|os\.(?:system|popen)|Popen|check_(?:output|call))\s*\(/;
246
256
  const STRING_LITERAL = /"([^"\n]*)"|'([^'\n]*)'/g;
247
257
  function pipLineTargets(line) {
@@ -1279,8 +1289,9 @@ export function createPythonAdapter(disabled, loadPyodide, network = false) {
1279
1289
  const packages = install
1280
1290
  ? [...new Set([...(prepared.packages ?? []), ...install])]
1281
1291
  : prepared.packages;
1282
- if (sandbox && install && !planVendoredInstall(packages ?? [])) {
1283
- return fail(pipMessage(install, { sandbox: true }));
1292
+ const refusal = sandbox ? sandboxInstallError(command, prepared.packages) : null;
1293
+ if (refusal) {
1294
+ return fail(refusal);
1284
1295
  }
1285
1296
  const result = await runPython(install ? 'pass' : source, {
1286
1297
  timeoutMs: options.timeoutMs,
@@ -28,8 +28,8 @@ interface WheelFs {
28
28
  exists: boolean;
29
29
  };
30
30
  }
31
- export declare function wheelsDir(): string | undefined;
32
- export declare function vendoredManifest(dir?: string | undefined): WheelManifest | null;
31
+ export declare function wheelsDir(): string;
32
+ export declare function vendoredManifest(dir?: string): WheelManifest | null;
33
33
  export declare function normalizeDistribution(name: string): string;
34
34
  export interface VendoredOptions {
35
35
  readonly dir?: string | undefined;
@@ -38,5 +38,5 @@ export interface VendoredOptions {
38
38
  export declare function vendoredSpec(distribution: string, opts?: VendoredOptions): string | null;
39
39
  export declare function planVendoredInstall(specs: readonly string[], opts?: VendoredOptions): VendoredInstall | null;
40
40
  export declare function stageVendoredWheels(target: WheelFs, install: VendoredInstall): string[];
41
- export declare function seedPackageCache(cacheDir: string, dir?: string | undefined): void;
41
+ export declare function seedPackageCache(cacheDir: string, dir?: string): void;
42
42
  export {};
@@ -16,20 +16,9 @@ const ManifestSchema = z.object({
16
16
  });
17
17
  const STAGE_DIR = '/qvac-wheels';
18
18
  export function wheelsDir() {
19
- const fromHost = process.env.QVAC_PYTHON_WHEELS_DIR;
20
- if (fromHost) {
21
- return fromHost;
22
- }
23
- const root = new URL('../../../../../', import.meta.url);
24
- const source = new URL(root.pathname.endsWith('/dist-lib/') ? '../wheels' : 'wheels', root);
25
- if (source.href.includes('.bundle/')) {
26
- return undefined;
27
- }
28
- return fileURLToPath(source.href);
19
+ return process.env.QVAC_PYTHON_WHEELS_DIR || fileURLToPath(import.meta.asset('#pyodide-wheels'));
29
20
  }
30
21
  export function vendoredManifest(dir = wheelsDir()) {
31
- if (!dir)
32
- return null;
33
22
  const file = `${dir}/manifest.json`;
34
23
  if (!fs.existsSync(file)) {
35
24
  hlog('python: no vendored wheels', { dir });
@@ -70,7 +59,7 @@ export function vendoredSpec(distribution, opts = {}) {
70
59
  export function planVendoredInstall(specs, opts = {}) {
71
60
  const dir = opts.dir ?? wheelsDir();
72
61
  const manifest = opts.manifest === undefined ? vendoredManifest(dir) : opts.manifest;
73
- if (!manifest || !dir || specs.length === 0) {
62
+ if (!manifest || specs.length === 0) {
74
63
  return null;
75
64
  }
76
65
  const wheels = new Set();
@@ -108,7 +97,7 @@ export function stageVendoredWheels(target, install) {
108
97
  }
109
98
  export function seedPackageCache(cacheDir, dir = wheelsDir()) {
110
99
  const manifest = vendoredManifest(dir);
111
- if (!manifest || !dir) {
100
+ if (!manifest) {
112
101
  return;
113
102
  }
114
103
  fs.mkdirSync(cacheDir, { recursive: true });
@@ -0,0 +1,19 @@
1
+ import { type WorkerMessage } from './protocol.ts';
2
+ import type { WorkerLink } from './spawn.ts';
3
+ export interface ActiveWorker {
4
+ readonly link: WorkerLink;
5
+ readonly view: Uint8Array;
6
+ readonly ready: Promise<void>;
7
+ nextId: number;
8
+ }
9
+ export declare function activeWorker(boot: () => ActiveWorker): ActiveWorker;
10
+ export declare function forget(worker: ActiveWorker): void;
11
+ export declare function destroy(worker: ActiveWorker): void;
12
+ export declare function readMessage(link: WorkerLink): Promise<WorkerMessage | null>;
13
+ export declare function drainAndReap(worker: ActiveWorker): Promise<void>;
14
+ export interface Grace {
15
+ expired: Promise<'grace'>;
16
+ clear: () => void;
17
+ }
18
+ export declare function grace(ms: number): Grace;
19
+ export declare function closePythonWorker(): Promise<void>;