@qvac/harness 0.6.0 → 0.7.1

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 (65) hide show
  1. package/CHANGELOG.md +132 -1
  2. package/README.md +47 -29
  3. package/dist-lib/index.d.ts +4 -0
  4. package/dist-lib/index.js +31 -19
  5. package/dist-lib/lib/capabilities/preview-model-source.d.ts +0 -1
  6. package/dist-lib/lib/connect.js +5 -2
  7. package/dist-lib/lib/contract-types.d.ts +1 -0
  8. package/dist-lib/lib/docx/index.d.ts +1 -1
  9. package/dist-lib/lib/docx/index.js +1 -1
  10. package/dist-lib/lib/hardware/capability-probe.js +9 -4
  11. package/dist-lib/lib/hooks/built-in/sandbox.js +0 -5
  12. package/dist-lib/lib/models/model-catalog-constants.d.ts +7 -0
  13. package/dist-lib/lib/models/model-catalog-constants.js +17 -0
  14. package/dist-lib/lib/models/model-catalog.d.ts +14 -6
  15. package/dist-lib/lib/models/model-catalog.js +14 -6
  16. package/dist-lib/lib/models/model-handle.d.ts +5 -1
  17. package/dist-lib/lib/models/model-handle.js +8 -3
  18. package/dist-lib/lib/models/model-list.d.ts +7 -3
  19. package/dist-lib/lib/models/model-list.js +9 -4
  20. package/dist-lib/lib/models/model-pool.d.ts +4 -1
  21. package/dist-lib/lib/models/model-pool.js +33 -10
  22. package/dist-lib/lib/models/model-task.d.ts +3 -0
  23. package/dist-lib/lib/models/model-task.js +14 -0
  24. package/dist-lib/lib/models/music-catalog.d.ts +2 -0
  25. package/dist-lib/lib/models/music-catalog.js +9 -0
  26. package/dist-lib/lib/office/index.d.ts +1 -1
  27. package/dist-lib/lib/office/index.js +1 -1
  28. package/dist-lib/lib/office/python.js +1 -2
  29. package/dist-lib/lib/serve.js +1 -1
  30. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
  31. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
  32. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
  33. package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
  34. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
  35. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
  36. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
  37. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
  38. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
  39. package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
  40. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
  45. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
  46. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
  47. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
  48. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
  49. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
  50. package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
  51. package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
  52. package/dist-lib/lib/tools/exec.d.ts +0 -2
  53. package/dist-lib/lib/tools/exec.js +6 -11
  54. package/dist-lib/schema/harness.js +4 -2
  55. package/dist-lib/spec/hrpc/messages.js +11 -8
  56. package/dist-lib/spec/hyperschema/index.js +11 -8
  57. package/dist-lib/spec/hyperschema/schema.json +11 -1
  58. package/dist-lib/spec/hyperschema/types.d.ts +3 -1
  59. package/package.json +39 -20
  60. package/dist/harness.bundle +0 -42313
  61. package/dist/harness.cjs +0 -18
  62. package/dist/harness.d.ts +0 -8
  63. package/dist/prebuilds-manifest.json +0 -372
  64. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
  65. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
package/CHANGELOG.md CHANGED
@@ -8,6 +8,135 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.7.1] - 2026-09-30
12
+
13
+ ### Added
14
+
15
+ - `list()` rows carry `isLoading`: true from the moment a load of that model
16
+ starts, whoever asked for it, until it is ready or the load fails. It is read
17
+ off the pool, so a host shows a model as loading without counting loads
18
+ itself. A model loading again under other settings while a copy of it is
19
+ resident reads `isLoaded` and `isLoading` both true. Chat models read it
20
+ today: image, music and translation models load for each generation, and
21
+ speech, transcription and embedding pools key by source, so those rows read
22
+ `false` for now. An optional field on the wire; a row from an older harness
23
+ reads `false`. (#143)
24
+
25
+ ```js
26
+ const loading = (await harness.list({ cachedOnly: true })).filter((row) => row.isLoading)
27
+ ```
28
+
29
+ ### Changed
30
+
31
+ - The `model` event carries `catalogChanged` at every edge a `list()` re-read
32
+ would show: the `loading` that starts a load and the `ready` or `error` that
33
+ ends it, beside `unloaded`. Progress ticks still do not. A host that re-reads
34
+ the list on `catalogChanged` sees a model turn loading, then loaded, with no
35
+ bookkeeping of its own. The served event stream never drops a frame that
36
+ carries it, so two `ready` frames of one model under two keys both cross. (#143)
37
+ - `ModelInfo` requires `isLoading`, and `listModels`, `customModelRows` and
38
+ `mergeCustomRows` return `CatalogRow`, the row before the pools are asked. A
39
+ typed fake `list()` adds the field. (#143)
40
+ - A custom model's row reads `isLoaded` only once its load has landed; it used
41
+ to read `true` from the first attempt. (#143)
42
+
43
+ ### Fixed
44
+
45
+ - A load that ends without the model in memory emits `model` with `error` and
46
+ the reason: it failed, or a suspend or close cancelled it. It used to end in
47
+ silence, so a host could not tell a failed load from one still running. The
48
+ `error` also refreshes the cached `isCached`, since a load may have downloaded
49
+ the weights before it failed. (#143)
50
+
51
+ ## [0.7.0] - 2026-09-29
52
+
53
+ ### Added
54
+
55
+ - `list()` rows carry `taskType`, what the model is for: `llm`, `embeddings`,
56
+ `speech-to-text`, `text-to-speech`, `image-generation`, `audio-generation`,
57
+ `translation`, `text-recognition` or `other`. It is decided from the addon in
58
+ one place, `lib/models/model-task.ts`, on each side of the wire rather than
59
+ sent over it. `ModelTaskType` is exported, and grows the four new values a
60
+ preview never returns. (#140)
61
+
62
+ - `modelsForAgent(agent, host)` lists every model an agent runs on a device,
63
+ each with its `use`: the chat model, clamped by `runnableModelName`, then the
64
+ image and music models its skills need. A host deciding what to download, or
65
+ which agents a model delete affects, asks this instead of working it out.
66
+ `runnableModelName(requested, host)` and `isMobilePlatform(platform)` move here
67
+ from `@qvac/core`: on a phone a pick above the device's profile runs as its
68
+ recommended model. Whether the host is a phone is the required `mobile` on
69
+ `HostCapability`, which `capability()` now reports, so its answer passes
70
+ straight in; a host built by hand has to say. Both functions are on the
71
+ browser-safe `@qvac/harness/model-catalog` entry too. (#141)
72
+
73
+ ```js
74
+ const chat = modelsForAgent(agent, await harness.capability()).find((m) => m.use === 'chat')
75
+ ```
76
+
77
+ - `capability()` answers `mobile`, whether the harness runs on a phone, read off
78
+ the platform it probes. It rides `capability-response` as an optional field;
79
+ a response from an older harness reads as `false`. (#141)
80
+
81
+ - `musicModelChoiceFrom(settings)` reads the music model pick out of an agent's
82
+ `generate_music` tool settings, so a host no longer knows what `modelChoice`
83
+ means there. `isMusicModelChoice` is exported beside it. (#141)
84
+
85
+ ### Changed
86
+
87
+ - **Breaking:** the package no longer publishes a prebuilt sidecar:
88
+ `dist/harness.bundle`, `dist/harness.cjs`, `dist/prebuilds-manifest.json` and
89
+ the `./dist/harness.cjs` export are gone. A host stows
90
+ `worker/harness-sidecar.mjs` itself, as desktop and mobile already do, or
91
+ spawns `worker/stow-entry.mjs` straight from its install. (#127)
92
+ - **Breaking:** a stowed harness runs python in a worker rather than in-process.
93
+ A host that stows the harness stops deferring `pyodide`, keeps
94
+ `QVAC_PYODIDE_CACHE_DIR` writable, no longer stages a separate worker bundle,
95
+ and ships what stow offloads beside the bundle, which now includes pyodide's
96
+ wasm, stdlib and lock file and the vendored wheels. A target that embeds
97
+ assets instead of offloading them cannot run python, since `fs` cannot read
98
+ inside a bundle. (#127)
99
+ - A bundle of the package root carries neither pyodide nor the vendored wheels.
100
+ Only an entry that runs python, the exec tool or `@qvac/harness/docx` and
101
+ `/office`, pulls them in. (#127)
102
+ - `SandboxHook` no longer checks `packages`. The exec tool refuses an install
103
+ the vendored wheels do not hold in full in sandbox mode, whether declared in
104
+ `packages` or on a pip line, with the same message as before. (#127)
105
+ - The vendored wheels are the `#pyodide-wheels` directory asset on Bare;
106
+ `QVAC_PYTHON_WHEELS_DIR` still overrides it. (#127)
107
+ - The python worker and the in-process deadline are spawned through
108
+ `worker_threads`, which the package maps to `bare-worker`;
109
+ `#pyodide-worker-spawn` is gone. (#127)
110
+ - **Breaking:** `engines.bare` is `>=1.34.0`, so Bare refuses to load the
111
+ package on older runtimes. `bare-runtime` is pinned to 1.34.0 and
112
+ `bare-sidecar` to 0.5.7 (Bare 1.34.0) across the tree, `bare-supervisor`'s copy
113
+ included, with `bare-thread` at least 1.2.7 and `bare-module` 7.1.0 for
114
+ `bare-worker`'s copy. (#127)
115
+ - `bare-module` is no longer a dependency or an override, and
116
+ `bare-module-traverse` is overridden to 2.5.8: earlier versions drop the
117
+ `asset` condition on `bare-sidecar`'s binary, and a stowed harness then fails
118
+ to boot on Bare 1.33 and later. (#127)
119
+ - `skillModelsForAgent` rows carry `use`, `image` or `music`, so a caller can
120
+ tell which skill a model is for without looking the name up in the catalogs. (#141)
121
+ - `CapabilityInfo` has a required `mobile`, so a `HarnessContract` whose
122
+ `capability()` builds its own answer, a test stub included, returns it too. (#141)
123
+
124
+ ### Removed
125
+
126
+ - **Breaking:** `bareRuntime` is no longer exported from `./lib/tools/exec.ts`.
127
+ It is at `./lib/tools/exec/bare-runtime.ts`, so the package root no longer
128
+ loads the exec tool to reach it. (#127)
129
+ - **Breaking:** `QVAC_PYODIDE_TEST_LOADER`: tests point
130
+ `QVAC_PYODIDE_WORKER_ENTRY` at an entry that serves their fake interpreter. (#127)
131
+
132
+ ### Fixed
133
+
134
+ - Python runs start on Bare 1.34, whose threads cannot read a module off disk,
135
+ including in a stowed harness. The worker is spawned through `worker_threads`,
136
+ which the package maps to `bare-worker` on Bare, pyodide is handed its asm
137
+ module as `createPyodideModule`, and the files pyodide reads are named as
138
+ assets, so a bundler carries them beside the bundle. (#127)
139
+
11
140
  ## [0.6.0] - 2026-09-28
12
141
 
13
142
  ### Added
@@ -890,7 +1019,9 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
890
1019
  - GPU stats are read through `bare-gpu-info`, so every dependency installs from
891
1020
  the public npm registry. (#12)
892
1021
 
893
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...HEAD
1022
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.1...HEAD
1023
+ [0.7.1]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...v0.7.1
1024
+ [0.7.0]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...v0.7.0
894
1025
  [0.6.0]: https://github.com/tetherto/qvac-harness/compare/v0.5.0...v0.6.0
895
1026
  [0.5.0]: https://github.com/tetherto/qvac-harness/compare/v0.4.0...v0.5.0
896
1027
  [0.4.0]: https://github.com/tetherto/qvac-harness/compare/v0.3.3...v0.4.0
package/README.md CHANGED
@@ -12,41 +12,40 @@ the supervision contract.
12
12
 
13
13
  ## The published package
14
14
 
15
- The published package ships the prebuilt engine and its runtime content, not
16
- the library source:
17
-
18
- - `dist/harness.bundle` — the stowed sidecar (all platforms in one bundle)
19
- - `dist/prebuilds-manifest.json` — every native prebuild dir the bundle defers,
20
- keyed by bundle-relative path; the host assembles those beside the bundle
21
- from its own `node_modules`
15
+ The published package ships the library and its runtime content, not a prebuilt
16
+ engine:
17
+
18
+ - `dist-lib/` — the compiled library and its declarations. The package exports
19
+ resolve to it; the TypeScript source stays in the repository.
20
+ - `worker/harness-sidecar.mjs` — the sidecar entry a host stows into its own
21
+ bundle, and `worker/stow-entry.mjs`, the same entry as a plain file for a
22
+ host that spawns it straight from its install
22
23
  - the hash-pinned Python `wheels/` the tool runtime consumes (skills come from
23
24
  [`@qvac/skills`](https://github.com/tetherto/qvac-skills), a content-addressed
24
25
  bundle this package materializes at runtime)
25
26
 
26
- The `@qvac` inference addons and `pyodide` resolve from the host's real
27
- `node_modules` at runtime (dlopen'd backends need real paths), so the host
28
- installs those alongside.
29
-
30
- The host-side library surface — `spawnHarness`/`harnessChild`, the transport,
31
- model catalogs, skill helpers and DOCX extractor — ships as compiled JavaScript
32
- and declarations in `dist-lib/`. The package exports resolve to that surface;
33
- the TypeScript source stays in the repository.
27
+ A host that stows the harness defers the `@qvac` inference addons
28
+ (`scripts/addon-externals.cjs` lists them), so they resolve from real files
29
+ beside the bundle, and ships everything stow offloads there: prebuilds, and
30
+ pyodide's data and the wheels, which the harness names as assets. A target that
31
+ embeds assets instead cannot run python.
34
32
 
35
33
  ## Spawning
36
34
 
37
- The generated `dist/harness.cjs` is the host entry: it spawns the bundle as a
38
- bare-sidecar process and hands back the stow ipc plus the exit promise.
35
+ `spawnHarness` spawns an entry as a bare-sidecar process and hands back a
36
+ client over hrpc:
39
37
 
40
38
  ```js
41
- const harness = require('@qvac/harness/dist/harness.cjs')
39
+ import { spawnHarness } from '@qvac/harness'
42
40
 
43
- const { ipc, sidecar, exit } = await harness.start(args)
41
+ const harness = spawnHarness({ entry, sdkStorage })
44
42
  ```
45
43
 
46
- The argv slots the bundle reads (skills dir, sdk storage, model config, wheels
47
- dir, ...) are the ones `worker/harness-sidecar.mjs` documents in this repo. The
48
- in-house hosts don't call this directly — they use the repo's library surface
49
- (`harnessChild` under bare-supervisor), which owns that argv contract.
44
+ `entry` is the host's own stowed bundle, or `@qvac/harness/worker/stow-entry.mjs`
45
+ resolved from its install. The argv slots the entry reads (skills dir, sdk
46
+ storage, model config, wheels dir, ...) are the ones
47
+ `worker/harness-sidecar.mjs` documents; `harnessChild` does the same under
48
+ bare-supervisor.
50
49
 
51
50
  ## Custom models
52
51
 
@@ -166,9 +165,11 @@ const custom = models.filter((model) => model.registrySource === 'custom')
166
165
  // registrySource: 'custom',
167
166
  // isCached: false,
168
167
  // isLoaded: false,
168
+ // isLoading: false,
169
169
  // supportsImages: false,
170
170
  // expectedSize: 1868506272,
171
- // label: 'Nanbeige 4.2 3B'
171
+ // label: 'Nanbeige 4.2 3B',
172
+ // taskType: 'llm'
172
173
  // }
173
174
  ```
174
175
 
@@ -181,6 +182,11 @@ sidecar knows from `sdkStorage.models`; an in-process `Harness` needs
181
182
  `new Harness({ modelsDir })` for it. A sharded download is looked for in the
182
183
  folder the SDK keeps its shards in, and is cached once every shard is there.
183
184
 
185
+ Every row, catalog or custom, carries `taskType`: what the model is for, in the
186
+ vocabulary a preview uses, so a Models screen groups by task rather than by
187
+ addon. `whisper` and `parakeet` are both `speech-to-text`, and an addon with no
188
+ task of its own is `other`.
189
+
184
190
  ### Delete a download
185
191
 
186
192
  ```js
@@ -229,8 +235,20 @@ if (big.reason === 'memory') showNotice('Unload a model first')
229
235
  `unloadModel` waits for any request holding the model to finish, then unloads
230
236
  every copy of it. `unloaded` is false when a request still held it after the
231
237
  wait, so a caller that wants the model gone at once stops those requests first.
232
- Both change `isLoaded` in the next `list()`, and emit a `model` event with
233
- `catalogChanged` set. A load or an unload counts as a call in flight for
238
+ Both change `isLoaded` in the next `list()`. `isLoading` is true on a row from
239
+ the moment a load of its model starts, whoever asked for it (a request,
240
+ `warmModel` or `loadModel`), until the model is ready or the load fails; it is
241
+ read off the pool, so a host keeps no count of its own. A model loading again
242
+ under other settings while a copy of it is resident reads `isLoaded: true` and
243
+ `isLoading: true` at once. The `model` event carries `catalogChanged` at each
244
+ edge a `list()` re-read would show: the `loading` that starts a load, the
245
+ `ready` or `error` that ends it, and `unloaded`. Progress ticks between them do
246
+ not. A load that ends without the model in memory reports
247
+ `error` with the reason in `message`: it failed, or a suspend or close cancelled it.
248
+ An image, music or translation model loads for each generation and never reads
249
+ `isLoading: true`; a speech, transcription or embedding model loads under its
250
+ source rather than its row name, so its row reads `false` for now. A load or an
251
+ unload counts as a call in flight for
234
252
  [`setModelIdleTimeout`](#freeing-memory-when-idle), and one that lands during an
235
253
  idle unload waits for it, so a load never loads a second copy and an unload
236
254
  answers once the memory is free.
@@ -273,8 +291,8 @@ are not extracted. Legacy XLS/PPT formats are unsupported. File extensions selec
273
291
  format, with `mimeType` as a fallback when the name has no recognized Office extension.
274
292
 
275
293
  The extractor accepts up to 20 MiB and requires the shipped Python wheels. With the
276
- cache configured, extraction uses those wheels offline. Bundled hosts also set `QVAC_PYODIDE_WORKER_ENTRY` and
277
- `QVAC_PYTHON_WHEELS_DIR` to their staged worker and wheels paths.
294
+ cache configured, extraction uses those wheels offline. A host that stows the harness ships them
295
+ with what stow offloads beside the bundle; `QVAC_PYTHON_WHEELS_DIR` names another directory.
278
296
  Source checkouts prepare wheels with `bun run vendor:wheels`.
279
297
 
280
298
  The existing `extractDocxText` and `closeDocxWorker` exports from `@qvac/harness/docx`
@@ -287,7 +305,7 @@ bun install
287
305
  bun run lint # prettier + lunte + tsc
288
306
  bun run test # brittle, node and bare lanes
289
307
  bun run build:schema # regenerates spec/ (wire-compat rewind once v* tags exist)
290
- bun run build:bundle # stows dist/harness.bundle (--minify for the shipped form)
308
+ bun run build:bundle # stows the sidecar entry into dist/, to prove it still stows
291
309
  ```
292
310
 
293
311
  The schema baseline rewinds `spec/` against this repo's `v*` release tags and
@@ -38,6 +38,7 @@ export type { EmbedDevice } from './lib/models/embed-config.ts';
38
38
  export type { MemoryBudget } from './lib/hardware/memory-budget.ts';
39
39
  export * from './lib/models/model-catalog.ts';
40
40
  export * from './lib/models/model-list.ts';
41
+ export type { ModelTaskType } from './lib/models/model-task.ts';
41
42
  export type { DeleteModelResult, DeleteModelSummary } from './lib/models/model-delete.ts';
42
43
  export { harnessChild, spawnHarness, type HarnessChildOptions, type SpawnHarnessOptions } from './lib/spawn.ts';
43
44
  export { settingsDimension, settingsSteps } from './lib/tools/generate-image.ts';
@@ -217,6 +218,9 @@ export declare class Harness extends ReadyResource {
217
218
  describe(): Promise<HarnessInfo>;
218
219
  diagnostics(): HarnessDiagnostics;
219
220
  private _loadedModels;
221
+ private _loadingModels;
222
+ private _settledModels;
223
+ private _pools;
220
224
  private _loadPluginSdk;
221
225
  private _loadDiffusionSdk;
222
226
  private _slmTranslate;
package/dist-lib/index.js CHANGED
@@ -4,7 +4,7 @@ import process from 'process';
4
4
  import ReadyResource from 'ready-resource';
5
5
  import { probeGpuStats } from './lib/hardware/gpu-probe.js';
6
6
  import { probeCapabilityProfile } from './lib/hardware/capability-probe.js';
7
- import { closePythonWorker } from './lib/tools/exec/adapters/python/worker/client.js';
7
+ import { closePythonWorker } from './lib/tools/exec/adapters/python/worker/active.js';
8
8
  import { completion, imageSettingsFrom } from './lib/capabilities/completion.js';
9
9
  import { detectLanguage } from './lib/capabilities/detect-language.js';
10
10
  import { invoke } from './lib/capabilities/invoke.js';
@@ -24,7 +24,7 @@ import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
24
24
  import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
25
25
  import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
26
26
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
27
- import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
27
+ import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
28
28
  import { defaultContextSize } from './lib/models/context.js';
29
29
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
30
30
  import { createIdleReaper } from './lib/models/idle-unload.js';
@@ -46,7 +46,7 @@ import { hlog } from './lib/log.js';
46
46
  import { harness } from './spec/index.js';
47
47
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
48
48
  import { probeBinVersions } from './lib/tools/skills/bin-check.js';
49
- import { bareRuntime } from './lib/tools/exec.js';
49
+ import { bareRuntime } from './lib/tools/exec/bare-runtime.js';
50
50
  import { runCommand } from './lib/tools/exec/runner.js';
51
51
  import { createLocalRoots } from './lib/tools/fs/local.js';
52
52
  import { createRemoteRoots } from './lib/tools/fs/remote.js';
@@ -558,13 +558,22 @@ export class Harness extends ReadyResource {
558
558
  };
559
559
  }
560
560
  _loadedModels() {
561
- const pools = [
561
+ return this._pools().flatMap((pool) => loadedModels(pool));
562
+ }
563
+ _loadingModels() {
564
+ return new Set(this._pools().flatMap((pool) => loadingModels(pool)));
565
+ }
566
+ _settledModels() {
567
+ return new Set(this._pools().flatMap((pool) => settledModels(pool)));
568
+ }
569
+ _pools() {
570
+ return [
571
+ this._models,
562
572
  this._embedModels,
563
573
  ...this._retiredEmbedModels,
564
574
  this._ttsModels,
565
575
  this._transcriptionModels
566
576
  ];
567
- return [...loadedModels(this._models), ...pools.flatMap((pool) => loadedModels(pool))];
568
577
  }
569
578
  _loadPluginSdk(current, store, importPlugin) {
570
579
  let pending = current();
@@ -667,8 +676,7 @@ export class Harness extends ReadyResource {
667
676
  });
668
677
  }
669
678
  _notifyModel(state) {
670
- const catalogChanged = state.status === 'ready' || state.status === 'unloaded';
671
- if (catalogChanged) {
679
+ if (state.status === 'ready' || state.status === 'unloaded' || state.status === 'error') {
672
680
  invalidateModelStatusCache(this._modelStatus);
673
681
  }
674
682
  if (state.status === 'unloaded') {
@@ -678,7 +686,11 @@ export class Harness extends ReadyResource {
678
686
  }
679
687
  }
680
688
  }
681
- this.emit('model', { ...state, catalogChanged, loaded: this._loadedModels() });
689
+ this.emit('model', {
690
+ ...state,
691
+ catalogChanged: state.catalogChanged === true,
692
+ loaded: this._loadedModels()
693
+ });
682
694
  }
683
695
  async _collectRetiredEmbedPool(pool) {
684
696
  if (!this._retiredEmbedModels.has(pool) || pool.idle !== pool.size)
@@ -1055,17 +1067,17 @@ export class Harness extends ReadyResource {
1055
1067
  if (!this.opened)
1056
1068
  await this.ready();
1057
1069
  const sdk = await this._sdk.load();
1058
- const [rows, custom] = await Promise.all([
1059
- listModels(sdk, options, {
1060
- cache: this._modelStatus,
1061
- projectionFor: (name) => this._projectionFor(name)
1062
- }),
1063
- customModelRows(options, {
1064
- modelsDir: this._modelsDir,
1065
- loaded: new Set(this._loadedModels().map((entry) => entry.model))
1066
- })
1067
- ]);
1068
- return mergeCustomRows(rows, custom);
1070
+ const rows = await listModels(sdk, options, {
1071
+ cache: this._modelStatus,
1072
+ projectionFor: (name) => this._projectionFor(name)
1073
+ });
1074
+ const loaded = this._settledModels();
1075
+ const loading = this._loadingModels();
1076
+ const custom = await customModelRows(options, { modelsDir: this._modelsDir, loaded });
1077
+ return mergeCustomRows(rows, custom).map((row) => ({
1078
+ ...row,
1079
+ isLoading: loading.has(row.name)
1080
+ }));
1069
1081
  }
1070
1082
  async deleteModel(name) {
1071
1083
  if (!this.opened)
@@ -1,7 +1,6 @@
1
1
  import type { HarnessPreviewModelSourceResponse } from '../../spec/hyperschema/types.d.ts';
2
2
  export type ModelFitVerdict = 'likely-fits' | 'likely-too-large' | 'unknown';
3
3
  export type ModelSourceRefusal = 'invalid-source' | 'not-found' | 'private' | 'gated' | 'web-page' | 'unreachable' | 'not-gguf' | 'not-a-model' | 'no-model-files' | 'invalid-projection';
4
- export type ModelTaskType = 'llm' | 'embeddings' | 'speech-to-text' | 'text-to-speech' | 'image-generation';
5
4
  export interface ModelSourceFile {
6
5
  src: string;
7
6
  label: string;
@@ -8,6 +8,7 @@ import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
8
8
  import { readModelSourcePreview } from './capabilities/preview-model-source.js';
9
9
  import { hlog } from './log.js';
10
10
  import { assertEmbedDevice } from './models/embed-config.js';
11
+ import { addonTaskType } from './models/model-task.js';
11
12
  import { errorCode, errorMessage, isRecord } from './guards.js';
12
13
  import { pushQueue } from './queue.js';
13
14
  import { stageFile, sweepStaging } from './staging.js';
@@ -568,7 +569,8 @@ export class RemoteHarness extends EventEmitter {
568
569
  return (response.models ?? []).map(({ actualSize, label, ...row }) => ({
569
570
  ...row,
570
571
  actualSize: actualSize || undefined,
571
- label: label || undefined
572
+ label: label || undefined,
573
+ taskType: addonTaskType(row.type)
572
574
  }));
573
575
  }
574
576
  async deleteModel(name) {
@@ -624,7 +626,8 @@ export class RemoteHarness extends EventEmitter {
624
626
  source: memoryBudget.source,
625
627
  gpuClass: memoryBudget.gpuClass || undefined
626
628
  }
627
- : undefined
629
+ : undefined,
630
+ mobile: response.mobile === true
628
631
  };
629
632
  }
630
633
  close() {
@@ -175,6 +175,7 @@ export interface CapabilityInfo {
175
175
  capabilityProfile: HarnessCapabilityProfile;
176
176
  recommendedModelName: string;
177
177
  memoryBudget?: MemoryBudget;
178
+ mobile: boolean;
178
179
  }
179
180
  export interface CredentialEntry {
180
181
  key: string;
@@ -1,5 +1,5 @@
1
1
  import type { AbortSignal } from 'bare-abort-controller';
2
- export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/client.ts';
2
+ export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/active.ts';
3
3
  export interface DocxExtractOptions {
4
4
  readonly signal?: AbortSignal;
5
5
  }
@@ -1,5 +1,5 @@
1
1
  import { extractWithPython } from '../office/python.js';
2
- export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/client.js';
2
+ export { closePythonWorker as closeDocxWorker } from '../tools/exec/adapters/python/worker/active.js';
3
3
  const PACKAGES = ['python-docx==1.2.0'];
4
4
  const EXTRACT = `from docx import Document
5
5
  from docx.text.paragraph import Paragraph
@@ -1,5 +1,5 @@
1
1
  import process from 'process';
2
- import { capabilityProfileToModelName } from '../models/model-catalog.js';
2
+ import { capabilityProfileToModelName, isMobilePlatform } from '../models/model-catalog.js';
3
3
  import { probeWithAddon } from './gpu-probe-addon.js';
4
4
  import { mobileBudget, parseNvidiaSmiOutput, parseRocmSmiOutput, parseWindowsRegistryOutput, sharedRamBudget } from './parsers.js';
5
5
  const GIB = 1024 * 1024 * 1024;
@@ -85,7 +85,7 @@ async function isPixel(reads) {
85
85
  }
86
86
  async function probeMemoryBudget(reads) {
87
87
  const os = reads.platform();
88
- if (os === 'android' || os === 'ios') {
88
+ if (isMobilePlatform(os)) {
89
89
  return mobileBudget(reads.totalmem(), await isPixel(reads));
90
90
  }
91
91
  if (os === 'darwin') {
@@ -101,9 +101,14 @@ function forcedProfile() {
101
101
  return undefined;
102
102
  }
103
103
  export async function probeCapabilityProfile(reads) {
104
+ const mobile = isMobilePlatform(reads.platform());
104
105
  const forced = forcedProfile();
105
106
  if (forced) {
106
- return { capabilityProfile: forced, recommendedModelName: capabilityProfileToModelName(forced) };
107
+ return {
108
+ capabilityProfile: forced,
109
+ recommendedModelName: capabilityProfileToModelName(forced),
110
+ mobile
111
+ };
107
112
  }
108
- return recommend(await probeMemoryBudget(reads));
113
+ return { ...recommend(await probeMemoryBudget(reads)), mobile };
109
114
  }
@@ -1,4 +1,3 @@
1
- import { planVendoredInstall } from '../../tools/exec/adapters/python/wheels.js';
2
1
  import { bareBin, firstCommandToken } from '../../tools/exec/scope.js';
3
2
  const NETWORK_BINS = new Set([
4
3
  'curl',
@@ -33,10 +32,6 @@ export class SandboxHook {
33
32
  return;
34
33
  if (!ctx.sandboxMode || ctx.toolName !== 'exec')
35
34
  return;
36
- const packages = ctx.args.packages;
37
- if (Array.isArray(packages) && packages.length && !planVendoredInstall(packages.map(String))) {
38
- return { error: 'sandbox mode: installing packages reaches the network', guidance: true };
39
- }
40
35
  const bin = bareBin(firstCommandToken(String(ctx.args.command ?? '')).replace(/^.*[/\\]/, ''));
41
36
  if (bin && NETWORK_BINS.has(bin)) {
42
37
  return { error: `sandbox mode: ${bin} reaches the network`, guidance: true };
@@ -11,6 +11,13 @@ export declare const MEDPSY_4B = "medpsy-4b-q4_k_m-imat";
11
11
  export declare function isCapabilityProfile(value: string): value is CapabilityProfile;
12
12
  export declare function meetsMinCapabilityProfile(active: CapabilityProfile, required: CapabilityProfile): boolean;
13
13
  export declare function modelNameToCapabilityProfile(modelName: string): CapabilityProfile | undefined;
14
+ export interface HostCapability {
15
+ readonly capabilityProfile: string;
16
+ readonly recommendedModelName: string;
17
+ readonly mobile: boolean;
18
+ }
19
+ export declare function isMobilePlatform(platform: string): boolean;
20
+ export declare function runnableModelName(requested: string, host: HostCapability | null): string;
14
21
  export declare function capabilityProfileToModelName(profile: CapabilityProfile): string;
15
22
  export declare const AGENT_MODEL_NAMES: readonly ["Qwen3.5-2B-Q4_K_M", "Qwen3.5-4B-Q4_K_M", "Qwen3.5-9B-Q4_K_M", "Qwen3.6-27B-UD-Q4_K_XL", "Qwen3.6-35B-A3B-UD-Q4_K_M", "Qwen3.8-27B-UD-Q4_K_XL", "medpsy-1.7b-q4_k_m-imat", "medpsy-4b-q4_k_m-imat"];
16
23
  export declare function modelDisplayName(modelName: string): string;
@@ -32,6 +32,23 @@ const MODEL_NAME_TO_PROFILE = new Map([
32
32
  export function modelNameToCapabilityProfile(modelName) {
33
33
  return MODEL_NAME_TO_PROFILE.get(modelName);
34
34
  }
35
+ export function isMobilePlatform(platform) {
36
+ return platform === 'android' || platform === 'ios';
37
+ }
38
+ export function runnableModelName(requested, host) {
39
+ if (!host?.mobile || !isCapabilityProfile(host.capabilityProfile)) {
40
+ return requested;
41
+ }
42
+ const required = modelNameToCapabilityProfile(requested);
43
+ if (!required || meetsMinCapabilityProfile(host.capabilityProfile, required)) {
44
+ return requested;
45
+ }
46
+ const fallback = modelNameToCapabilityProfile(host.recommendedModelName);
47
+ if (!fallback || !meetsMinCapabilityProfile(required, fallback)) {
48
+ return requested;
49
+ }
50
+ return host.recommendedModelName;
51
+ }
35
52
  const PROFILE_TO_MODEL_NAME = new Map([
36
53
  ['tiny', QWEN_35_2B],
37
54
  ['low', QWEN_35_2B],
@@ -5,9 +5,9 @@ import type { ModelAddon } from './model-source.ts';
5
5
  export interface SdkModelAsset extends ModelRegistryEntry {
6
6
  readonly src: string;
7
7
  }
8
- 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, type CapabilityProfile, type ImageModelChoice, type ImageChoiceEntry } from './model-catalog-constants.ts';
9
- 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 };
10
- export type { CapabilityProfile, ImageModelChoice, ImageChoiceEntry };
8
+ 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, type CapabilityProfile, type HostCapability, type ImageModelChoice, type ImageChoiceEntry } from './model-catalog-constants.ts';
9
+ 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 };
10
+ export type { CapabilityProfile, HostCapability, ImageModelChoice, ImageChoiceEntry };
11
11
  export interface LlmCatalogEntry {
12
12
  readonly name: string;
13
13
  readonly weight: SdkModelAsset;
@@ -49,8 +49,8 @@ export interface DiffusionDownloadMetadata {
49
49
  }
50
50
  export declare function diffusionDownloadName(model: string): string | null;
51
51
  export declare function diffusionDownloadMetadata(model: string, addon: ModelAddon | undefined): DiffusionDownloadMetadata | undefined;
52
- import { DEFAULT_MUSIC_MODEL_CHOICE, type MusicModelChoice } from './music-catalog.ts';
53
- export { DEFAULT_MUSIC_MODEL_CHOICE, type MusicModelChoice };
52
+ import { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom, type MusicModelChoice } from './music-catalog.ts';
53
+ export { DEFAULT_MUSIC_MODEL_CHOICE, isMusicModelChoice, musicModelChoiceFrom, type MusicModelChoice };
54
54
  export interface AudioGenCatalogEntry {
55
55
  readonly choice: MusicModelChoice;
56
56
  readonly name: string;
@@ -78,7 +78,15 @@ export interface AgentSkillModelInput {
78
78
  readonly imageModelChoice?: ImageModelChoice | null;
79
79
  readonly musicModelChoice?: MusicModelChoice | null;
80
80
  }
81
- export declare function skillModelsForAgent(agent: AgentSkillModelInput): ReadonlyArray<CatalogModel>;
81
+ export type AgentModelUse = 'chat' | 'image' | 'music';
82
+ export interface AgentModel extends CatalogModel {
83
+ readonly use: AgentModelUse;
84
+ }
85
+ export interface AgentModelInput extends AgentSkillModelInput {
86
+ readonly modelName?: string | null;
87
+ }
88
+ export declare function skillModelsForAgent(agent: AgentSkillModelInput): ReadonlyArray<AgentModel>;
89
+ export declare function modelsForAgent(agent: AgentModelInput, host: HostCapability | null): ReadonlyArray<AgentModel>;
82
90
  export declare function diffusionSizeBytes(entry: DiffusionCatalogEntry): number;
83
91
  export declare function imageModelFor(choice: string | undefined): ImageModelSet | null;
84
92
  export interface ImageClamps {