@qvac/harness 0.6.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 (60) hide show
  1. package/CHANGELOG.md +91 -1
  2. package/README.md +32 -27
  3. package/dist-lib/index.d.ts +1 -0
  4. package/dist-lib/index.js +2 -2
  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-list.d.ts +2 -0
  17. package/dist-lib/lib/models/model-list.js +9 -4
  18. package/dist-lib/lib/models/model-task.d.ts +3 -0
  19. package/dist-lib/lib/models/model-task.js +14 -0
  20. package/dist-lib/lib/models/music-catalog.d.ts +2 -0
  21. package/dist-lib/lib/models/music-catalog.js +9 -0
  22. package/dist-lib/lib/office/index.d.ts +1 -1
  23. package/dist-lib/lib/office/index.js +1 -1
  24. package/dist-lib/lib/office/python.js +1 -2
  25. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.d.ts +10 -0
  26. package/dist-lib/lib/tools/exec/adapters/python/deadline-state.js +5 -0
  27. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +10 -17
  28. package/dist-lib/lib/tools/exec/adapters/python/loader.js +15 -11
  29. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +1 -15
  30. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +1 -0
  31. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +13 -2
  32. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +3 -3
  33. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +3 -14
  34. package/dist-lib/lib/tools/exec/adapters/python/worker/active.d.ts +19 -0
  35. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +72 -0
  36. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +0 -1
  37. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +8 -76
  38. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +2 -126
  39. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.d.ts +1 -0
  40. package/dist-lib/lib/tools/exec/adapters/python/worker/interrupt-deadline.js +21 -0
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.d.ts +2 -0
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +90 -0
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.d.ts → spawn.d.ts} +0 -1
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/{spawn-node.js → spawn.js} +0 -8
  45. package/dist-lib/lib/tools/exec/bare-runtime.d.ts +2 -0
  46. package/dist-lib/lib/tools/exec/bare-runtime.js +10 -0
  47. package/dist-lib/lib/tools/exec.d.ts +0 -2
  48. package/dist-lib/lib/tools/exec.js +6 -11
  49. package/dist-lib/schema/harness.js +2 -1
  50. package/dist-lib/spec/hrpc/messages.js +6 -5
  51. package/dist-lib/spec/hyperschema/index.js +6 -5
  52. package/dist-lib/spec/hyperschema/schema.json +6 -1
  53. package/dist-lib/spec/hyperschema/types.d.ts +2 -1
  54. package/package.json +39 -20
  55. package/dist/harness.bundle +0 -42313
  56. package/dist/harness.cjs +0 -18
  57. package/dist/harness.d.ts +0 -8
  58. package/dist/prebuilds-manifest.json +0 -372
  59. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.d.ts +0 -3
  60. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-bare.js +0 -38
package/CHANGELOG.md CHANGED
@@ -8,6 +8,95 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.7.0] - 2026-09-29
12
+
13
+ ### Added
14
+
15
+ - `list()` rows carry `taskType`, what the model is for: `llm`, `embeddings`,
16
+ `speech-to-text`, `text-to-speech`, `image-generation`, `audio-generation`,
17
+ `translation`, `text-recognition` or `other`. It is decided from the addon in
18
+ one place, `lib/models/model-task.ts`, on each side of the wire rather than
19
+ sent over it. `ModelTaskType` is exported, and grows the four new values a
20
+ preview never returns. (#140)
21
+
22
+ - `modelsForAgent(agent, host)` lists every model an agent runs on a device,
23
+ each with its `use`: the chat model, clamped by `runnableModelName`, then the
24
+ image and music models its skills need. A host deciding what to download, or
25
+ which agents a model delete affects, asks this instead of working it out.
26
+ `runnableModelName(requested, host)` and `isMobilePlatform(platform)` move here
27
+ from `@qvac/core`: on a phone a pick above the device's profile runs as its
28
+ recommended model. Whether the host is a phone is the required `mobile` on
29
+ `HostCapability`, which `capability()` now reports, so its answer passes
30
+ straight in; a host built by hand has to say. Both functions are on the
31
+ browser-safe `@qvac/harness/model-catalog` entry too. (#141)
32
+
33
+ ```js
34
+ const chat = modelsForAgent(agent, await harness.capability()).find((m) => m.use === 'chat')
35
+ ```
36
+
37
+ - `capability()` answers `mobile`, whether the harness runs on a phone, read off
38
+ the platform it probes. It rides `capability-response` as an optional field;
39
+ a response from an older harness reads as `false`. (#141)
40
+
41
+ - `musicModelChoiceFrom(settings)` reads the music model pick out of an agent's
42
+ `generate_music` tool settings, so a host no longer knows what `modelChoice`
43
+ means there. `isMusicModelChoice` is exported beside it. (#141)
44
+
45
+ ### Changed
46
+
47
+ - **Breaking:** the package no longer publishes a prebuilt sidecar:
48
+ `dist/harness.bundle`, `dist/harness.cjs`, `dist/prebuilds-manifest.json` and
49
+ the `./dist/harness.cjs` export are gone. A host stows
50
+ `worker/harness-sidecar.mjs` itself, as desktop and mobile already do, or
51
+ spawns `worker/stow-entry.mjs` straight from its install. (#127)
52
+ - **Breaking:** a stowed harness runs python in a worker rather than in-process.
53
+ A host that stows the harness stops deferring `pyodide`, keeps
54
+ `QVAC_PYODIDE_CACHE_DIR` writable, no longer stages a separate worker bundle,
55
+ and ships what stow offloads beside the bundle, which now includes pyodide's
56
+ wasm, stdlib and lock file and the vendored wheels. A target that embeds
57
+ assets instead of offloading them cannot run python, since `fs` cannot read
58
+ inside a bundle. (#127)
59
+ - A bundle of the package root carries neither pyodide nor the vendored wheels.
60
+ Only an entry that runs python, the exec tool or `@qvac/harness/docx` and
61
+ `/office`, pulls them in. (#127)
62
+ - `SandboxHook` no longer checks `packages`. The exec tool refuses an install
63
+ the vendored wheels do not hold in full in sandbox mode, whether declared in
64
+ `packages` or on a pip line, with the same message as before. (#127)
65
+ - The vendored wheels are the `#pyodide-wheels` directory asset on Bare;
66
+ `QVAC_PYTHON_WHEELS_DIR` still overrides it. (#127)
67
+ - The python worker and the in-process deadline are spawned through
68
+ `worker_threads`, which the package maps to `bare-worker`;
69
+ `#pyodide-worker-spawn` is gone. (#127)
70
+ - **Breaking:** `engines.bare` is `>=1.34.0`, so Bare refuses to load the
71
+ package on older runtimes. `bare-runtime` is pinned to 1.34.0 and
72
+ `bare-sidecar` to 0.5.7 (Bare 1.34.0) across the tree, `bare-supervisor`'s copy
73
+ included, with `bare-thread` at least 1.2.7 and `bare-module` 7.1.0 for
74
+ `bare-worker`'s copy. (#127)
75
+ - `bare-module` is no longer a dependency or an override, and
76
+ `bare-module-traverse` is overridden to 2.5.8: earlier versions drop the
77
+ `asset` condition on `bare-sidecar`'s binary, and a stowed harness then fails
78
+ to boot on Bare 1.33 and later. (#127)
79
+ - `skillModelsForAgent` rows carry `use`, `image` or `music`, so a caller can
80
+ tell which skill a model is for without looking the name up in the catalogs. (#141)
81
+ - `CapabilityInfo` has a required `mobile`, so a `HarnessContract` whose
82
+ `capability()` builds its own answer, a test stub included, returns it too. (#141)
83
+
84
+ ### Removed
85
+
86
+ - **Breaking:** `bareRuntime` is no longer exported from `./lib/tools/exec.ts`.
87
+ It is at `./lib/tools/exec/bare-runtime.ts`, so the package root no longer
88
+ loads the exec tool to reach it. (#127)
89
+ - **Breaking:** `QVAC_PYODIDE_TEST_LOADER`: tests point
90
+ `QVAC_PYODIDE_WORKER_ENTRY` at an entry that serves their fake interpreter. (#127)
91
+
92
+ ### Fixed
93
+
94
+ - Python runs start on Bare 1.34, whose threads cannot read a module off disk,
95
+ including in a stowed harness. The worker is spawned through `worker_threads`,
96
+ which the package maps to `bare-worker` on Bare, pyodide is handed its asm
97
+ module as `createPyodideModule`, and the files pyodide reads are named as
98
+ assets, so a bundler carries them beside the bundle. (#127)
99
+
11
100
  ## [0.6.0] - 2026-09-28
12
101
 
13
102
  ### Added
@@ -890,7 +979,8 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
890
979
  - GPU stats are read through `bare-gpu-info`, so every dependency installs from
891
980
  the public npm registry. (#12)
892
981
 
893
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...HEAD
982
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.7.0...HEAD
983
+ [0.7.0]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...v0.7.0
894
984
  [0.6.0]: https://github.com/tetherto/qvac-harness/compare/v0.5.0...v0.6.0
895
985
  [0.5.0]: https://github.com/tetherto/qvac-harness/compare/v0.4.0...v0.5.0
896
986
  [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
 
@@ -168,7 +167,8 @@ const custom = models.filter((model) => model.registrySource === 'custom')
168
167
  // isLoaded: false,
169
168
  // supportsImages: false,
170
169
  // expectedSize: 1868506272,
171
- // label: 'Nanbeige 4.2 3B'
170
+ // label: 'Nanbeige 4.2 3B',
171
+ // taskType: 'llm'
172
172
  // }
173
173
  ```
174
174
 
@@ -181,6 +181,11 @@ sidecar knows from `sdkStorage.models`; an in-process `Harness` needs
181
181
  `new Harness({ modelsDir })` for it. A sharded download is looked for in the
182
182
  folder the SDK keeps its shards in, and is cached once every shard is there.
183
183
 
184
+ Every row, catalog or custom, carries `taskType`: what the model is for, in the
185
+ vocabulary a preview uses, so a Models screen groups by task rather than by
186
+ addon. `whisper` and `parakeet` are both `speech-to-text`, and an addon with no
187
+ task of its own is `other`.
188
+
184
189
  ### Delete a download
185
190
 
186
191
  ```js
@@ -273,8 +278,8 @@ are not extracted. Legacy XLS/PPT formats are unsupported. File extensions selec
273
278
  format, with `mimeType` as a fallback when the name has no recognized Office extension.
274
279
 
275
280
  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.
281
+ cache configured, extraction uses those wheels offline. A host that stows the harness ships them
282
+ with what stow offloads beside the bundle; `QVAC_PYTHON_WHEELS_DIR` names another directory.
278
283
  Source checkouts prepare wheels with `bun run vendor:wheels`.
279
284
 
280
285
  The existing `extractDocxText` and `closeDocxWorker` exports from `@qvac/harness/docx`
@@ -287,7 +292,7 @@ bun install
287
292
  bun run lint # prettier + lunte + tsc
288
293
  bun run test # brittle, node and bare lanes
289
294
  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)
295
+ bun run build:bundle # stows the sidecar entry into dist/, to prove it still stows
291
296
  ```
292
297
 
293
298
  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';
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';
@@ -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';
@@ -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 {
@@ -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
  }
@@ -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;
@@ -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`);
@@ -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;