@qvac/harness 0.4.0 → 0.6.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 (299) hide show
  1. package/CHANGELOG.md +740 -141
  2. package/README.md +75 -7
  3. package/dist/harness.bundle +72 -50
  4. package/dist-lib/index.d.ts +49 -39
  5. package/dist-lib/index.js +448 -161
  6. package/dist-lib/lib/audio-transcode-bare.d.ts +2 -2
  7. package/dist-lib/lib/audio-transcode-bare.js +28 -18
  8. package/dist-lib/lib/audio-transcode-node.d.ts +2 -2
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +7 -6
  10. package/dist-lib/lib/capabilities/audio-resample.js +30 -18
  11. package/dist-lib/lib/capabilities/completion.d.ts +20 -19
  12. package/dist-lib/lib/capabilities/completion.js +193 -132
  13. package/dist-lib/lib/capabilities/download.d.ts +6 -6
  14. package/dist-lib/lib/capabilities/download.js +15 -8
  15. package/dist-lib/lib/capabilities/embed.js +2 -1
  16. package/dist-lib/lib/capabilities/enhance.d.ts +6 -1
  17. package/dist-lib/lib/capabilities/enhance.js +7 -4
  18. package/dist-lib/lib/capabilities/invoke.js +7 -4
  19. package/dist-lib/lib/capabilities/kv-cache.js +2 -1
  20. package/dist-lib/lib/capabilities/preview-model-source.d.ts +9 -7
  21. package/dist-lib/lib/capabilities/preview-model-source.js +80 -28
  22. package/dist-lib/lib/capabilities/speech-text.js +5 -4
  23. package/dist-lib/lib/capabilities/tool-selection.js +10 -9
  24. package/dist-lib/lib/capabilities/tool-turns.js +30 -16
  25. package/dist-lib/lib/capabilities/usage-breakdown.js +2 -1
  26. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  27. package/dist-lib/lib/capabilities/voice.js +22 -12
  28. package/dist-lib/lib/connect.d.ts +13 -32
  29. package/dist-lib/lib/connect.js +170 -112
  30. package/dist-lib/lib/context-window.js +25 -21
  31. package/dist-lib/lib/contract-types.d.ts +35 -15
  32. package/dist-lib/lib/diagram-repair.d.ts +4 -4
  33. package/dist-lib/lib/diagram-repair.js +6 -3
  34. package/dist-lib/lib/docx/index.d.ts +3 -2
  35. package/dist-lib/lib/{error-fields.d.ts → guards.d.ts} +1 -0
  36. package/dist-lib/lib/{error-fields.js → guards.js} +3 -0
  37. package/dist-lib/lib/hardware/capability-probe.js +20 -10
  38. package/dist-lib/lib/hardware/gpu-classify.js +6 -3
  39. package/dist-lib/lib/hardware/gpu-probe-addon.js +8 -4
  40. package/dist-lib/lib/hardware/gpu-probe.js +4 -2
  41. package/dist-lib/lib/hardware/gpu-stats.native.cjs +8 -9
  42. package/dist-lib/lib/hardware/parsers.js +8 -4
  43. package/dist-lib/lib/helpers/buffer-burst.js +4 -2
  44. package/dist-lib/lib/helpers/control-tokens.js +4 -2
  45. package/dist-lib/lib/helpers/join-guard.d.ts +2 -2
  46. package/dist-lib/lib/helpers/join-guard.js +4 -2
  47. package/dist-lib/lib/helpers/load-stall.js +4 -2
  48. package/dist-lib/lib/helpers/markup-stream-filter.d.ts +2 -2
  49. package/dist-lib/lib/helpers/markup-stream-filter.js +6 -3
  50. package/dist-lib/lib/helpers/race-abort.js +2 -1
  51. package/dist-lib/lib/helpers/race-timeout.d.ts +5 -0
  52. package/dist-lib/lib/helpers/race-timeout.js +14 -0
  53. package/dist-lib/lib/helpers/run-outcome.d.ts +6 -8
  54. package/dist-lib/lib/helpers/run-outcome.js +36 -20
  55. package/dist-lib/lib/helpers/spawn-path.js +6 -3
  56. package/dist-lib/lib/helpers/text-coalescer.js +6 -3
  57. package/dist-lib/lib/helpers/thinking.js +38 -45
  58. package/dist-lib/lib/helpers/tool-call-markup.d.ts +9 -7
  59. package/dist-lib/lib/helpers/tool-call-markup.js +36 -20
  60. package/dist-lib/lib/helpers/tool-intent.d.ts +3 -2
  61. package/dist-lib/lib/helpers/tool-intent.js +4 -2
  62. package/dist-lib/lib/helpers/tool-label.js +18 -9
  63. package/dist-lib/lib/hooks/built-in/arg-repair.js +23 -13
  64. package/dist-lib/lib/hooks/built-in/egress.js +2 -1
  65. package/dist-lib/lib/hooks/built-in/skill-catalog.js +2 -1
  66. package/dist-lib/lib/hooks/pipeline.d.ts +5 -4
  67. package/dist-lib/lib/hooks/pipeline.js +6 -3
  68. package/dist-lib/lib/hooks/run.js +12 -8
  69. package/dist-lib/lib/model-download-progress.js +10 -8
  70. package/dist-lib/lib/models/context.d.ts +1 -1
  71. package/dist-lib/lib/models/context.js +5 -1
  72. package/dist-lib/lib/models/custom-model-cache.d.ts +10 -1
  73. package/dist-lib/lib/models/custom-model-cache.js +43 -15
  74. package/dist-lib/lib/models/gguf-header.d.ts +3 -2
  75. package/dist-lib/lib/models/gguf-header.js +44 -22
  76. package/dist-lib/lib/models/idle-unload.d.ts +14 -0
  77. package/dist-lib/lib/models/idle-unload.js +51 -0
  78. package/dist-lib/lib/models/kv-cache.d.ts +3 -2
  79. package/dist-lib/lib/models/kv-cache.js +4 -3
  80. package/dist-lib/lib/models/llm-presets.d.ts +2 -2
  81. package/dist-lib/lib/models/llm-presets.js +4 -2
  82. package/dist-lib/lib/models/model-catalog-constants.js +3 -2
  83. package/dist-lib/lib/models/model-catalog.d.ts +15 -20
  84. package/dist-lib/lib/models/model-catalog.js +66 -31
  85. package/dist-lib/lib/models/model-delete.js +18 -8
  86. package/dist-lib/lib/models/model-handle.js +2 -1
  87. package/dist-lib/lib/models/model-list.d.ts +8 -3
  88. package/dist-lib/lib/models/model-list.js +11 -6
  89. package/dist-lib/lib/models/model-pool.d.ts +48 -10
  90. package/dist-lib/lib/models/model-pool.js +106 -45
  91. package/dist-lib/lib/models/model-source.js +10 -5
  92. package/dist-lib/lib/models/music-catalog.d.ts +2 -2
  93. package/dist-lib/lib/models/nmt-catalog.d.ts +4 -3
  94. package/dist-lib/lib/models/nmt-catalog.js +10 -5
  95. package/dist-lib/lib/models/opencl-cache.d.ts +6 -1
  96. package/dist-lib/lib/models/opencl-cache.js +5 -3
  97. package/dist-lib/lib/models/sdk-faults.d.ts +11 -8
  98. package/dist-lib/lib/models/sdk-faults.js +9 -6
  99. package/dist-lib/lib/models/shards.d.ts +11 -0
  100. package/dist-lib/lib/models/shards.js +15 -0
  101. package/dist-lib/lib/models/tool-dialect.js +10 -5
  102. package/dist-lib/lib/no-redirect-fetch-bare.d.ts +3 -3
  103. package/dist-lib/lib/no-redirect-fetch-bare.js +1 -1
  104. package/dist-lib/lib/no-redirect-fetch-impl.js +6 -3
  105. package/dist-lib/lib/no-redirect-fetch-node.d.ts +1 -1
  106. package/dist-lib/lib/no-redirect-fetch-node.js +9 -9
  107. package/dist-lib/lib/office/index.d.ts +3 -2
  108. package/dist-lib/lib/office/python.d.ts +3 -2
  109. package/dist-lib/lib/office/python.js +2 -1
  110. package/dist-lib/lib/prompt.d.ts +5 -4
  111. package/dist-lib/lib/prompt.js +8 -4
  112. package/dist-lib/lib/queue.js +10 -5
  113. package/dist-lib/lib/resolution-detector.js +21 -11
  114. package/dist-lib/lib/rpc-session.js +2 -1
  115. package/dist-lib/lib/sdk-loader.js +2 -1
  116. package/dist-lib/lib/sdk-storage.js +4 -2
  117. package/dist-lib/lib/serve.js +122 -66
  118. package/dist-lib/lib/spawn.js +7 -4
  119. package/dist-lib/lib/tmp-path.d.ts +7 -6
  120. package/dist-lib/lib/tool-call-display.d.ts +2 -2
  121. package/dist-lib/lib/tools/create-documents.js +18 -10
  122. package/dist-lib/lib/tools/edit-documents.js +23 -13
  123. package/dist-lib/lib/tools/edit-image.d.ts +2 -2
  124. package/dist-lib/lib/tools/edit-image.js +60 -36
  125. package/dist-lib/lib/tools/exec/adapters/adapter.d.ts +10 -10
  126. package/dist-lib/lib/tools/exec/adapters/python/attachments.d.ts +17 -11
  127. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +53 -40
  128. package/dist-lib/lib/tools/exec/adapters/python/create-effect.d.ts +2 -2
  129. package/dist-lib/lib/tools/exec/adapters/python/create-effect.js +60 -32
  130. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.d.ts +33 -22
  131. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +58 -29
  132. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.d.ts +2 -2
  133. package/dist-lib/lib/tools/exec/adapters/python/loader.js +23 -12
  134. package/dist-lib/lib/tools/exec/adapters/python/memfs.d.ts +13 -11
  135. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +10 -5
  136. package/dist-lib/lib/tools/exec/adapters/python/net-shim.js +3 -1
  137. package/dist-lib/lib/tools/exec/adapters/python/output-policy.d.ts +6 -4
  138. package/dist-lib/lib/tools/exec/adapters/python/output-policy.js +20 -10
  139. package/dist-lib/lib/tools/exec/adapters/python/pyodide.d.ts +2 -2
  140. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +14 -7
  141. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.d.ts +41 -34
  142. package/dist-lib/lib/tools/exec/adapters/python/python-adapter.js +316 -166
  143. package/dist-lib/lib/tools/exec/adapters/python/run-session.d.ts +9 -8
  144. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +56 -28
  145. package/dist-lib/lib/tools/exec/adapters/python/wheels.d.ts +8 -10
  146. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +22 -11
  147. package/dist-lib/lib/tools/exec/adapters/python/worker/client.d.ts +3 -2
  148. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +41 -26
  149. package/dist-lib/lib/tools/exec/adapters/python/worker/entry.js +27 -18
  150. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -2
  151. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.js +14 -13
  152. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.d.ts +4 -4
  153. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn-node.js +10 -5
  154. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.d.ts +4 -4
  155. package/dist-lib/lib/tools/exec/adapters/shell/shell-adapter.js +2 -1
  156. package/dist-lib/lib/tools/exec/obsidian/cli-process.d.ts +6 -1
  157. package/dist-lib/lib/tools/exec/obsidian/cli-process.js +8 -5
  158. package/dist-lib/lib/tools/exec/obsidian/cli.d.ts +6 -6
  159. package/dist-lib/lib/tools/exec/obsidian/cli.js +4 -4
  160. package/dist-lib/lib/tools/exec/obsidian/command.d.ts +3 -2
  161. package/dist-lib/lib/tools/exec/obsidian/command.js +6 -3
  162. package/dist-lib/lib/tools/exec/obsidian/output.js +2 -1
  163. package/dist-lib/lib/tools/exec/obsidian/route.d.ts +4 -6
  164. package/dist-lib/lib/tools/exec/obsidian/route.js +17 -11
  165. package/dist-lib/lib/tools/exec/obsidian/safe-path.js +2 -1
  166. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +15 -7
  167. package/dist-lib/lib/tools/exec/obsidian/vault-paths.d.ts +2 -2
  168. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +20 -10
  169. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +20 -13
  170. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +24 -12
  171. package/dist-lib/lib/tools/exec/runner.d.ts +14 -14
  172. package/dist-lib/lib/tools/exec/runner.js +5 -3
  173. package/dist-lib/lib/tools/exec/scope.js +52 -28
  174. package/dist-lib/lib/tools/exec/shell-argv.js +31 -18
  175. package/dist-lib/lib/tools/exec/shell-guidance.d.ts +6 -1
  176. package/dist-lib/lib/tools/exec/shell-guidance.js +35 -20
  177. package/dist-lib/lib/tools/exec/video-tools.js +2 -2
  178. package/dist-lib/lib/tools/exec.d.ts +6 -8
  179. package/dist-lib/lib/tools/exec.js +113 -54
  180. package/dist-lib/lib/tools/file-glob.d.ts +4 -5
  181. package/dist-lib/lib/tools/file-glob.js +16 -11
  182. package/dist-lib/lib/tools/fs/glob.js +13 -18
  183. package/dist-lib/lib/tools/fs/remote-protocol.d.ts +10 -10
  184. package/dist-lib/lib/tools/fs/remote.d.ts +3 -2
  185. package/dist-lib/lib/tools/fs/remote.js +13 -7
  186. package/dist-lib/lib/tools/fs/scope.d.ts +8 -3
  187. package/dist-lib/lib/tools/fs/scope.js +20 -12
  188. package/dist-lib/lib/tools/fs/walk.js +6 -3
  189. package/dist-lib/lib/tools/fs/write.d.ts +3 -2
  190. package/dist-lib/lib/tools/generate-image.d.ts +11 -9
  191. package/dist-lib/lib/tools/generate-image.js +73 -37
  192. package/dist-lib/lib/tools/generate-music.d.ts +22 -11
  193. package/dist-lib/lib/tools/generate-music.js +79 -48
  194. package/dist-lib/lib/tools/grep.js +66 -41
  195. package/dist-lib/lib/tools/http-request/encode-fields.js +18 -9
  196. package/dist-lib/lib/tools/http-request/{utils.js → request.js} +20 -11
  197. package/dist-lib/lib/tools/http-request/truncate-response.js +57 -46
  198. package/dist-lib/lib/tools/http-request.d.ts +1 -1
  199. package/dist-lib/lib/tools/http-request.js +33 -18
  200. package/dist-lib/lib/tools/mcp/protocol.d.ts +53 -0
  201. package/dist-lib/lib/tools/mcp/{utils.js → protocol.js} +45 -23
  202. package/dist-lib/lib/tools/mcp-call.d.ts +1 -1
  203. package/dist-lib/lib/tools/mcp-call.js +37 -16
  204. package/dist-lib/lib/tools/operations/builders.js +10 -6
  205. package/dist-lib/lib/tools/operations/compile.js +20 -10
  206. package/dist-lib/lib/tools/operations/decoders.js +12 -6
  207. package/dist-lib/lib/tools/operations/gmail-message.js +3 -2
  208. package/dist-lib/lib/tools/outbound-guard.d.ts +4 -3
  209. package/dist-lib/lib/tools/outbound-guard.js +14 -8
  210. package/dist-lib/lib/tools/private-ip.js +8 -7
  211. package/dist-lib/lib/tools/rag-search.js +14 -7
  212. package/dist-lib/lib/tools/read-files.js +23 -20
  213. package/dist-lib/lib/tools/skill.js +19 -10
  214. package/dist-lib/lib/tools/skills/auth-resolver.d.ts +11 -18
  215. package/dist-lib/lib/tools/skills/auth-resolver.js +30 -18
  216. package/dist-lib/lib/tools/skills/bin-check.d.ts +3 -2
  217. package/dist-lib/lib/tools/skills/bin-check.js +10 -3
  218. package/dist-lib/lib/tools/skills/bundled-dir.js +20 -10
  219. package/dist-lib/lib/tools/skills/bundled-hash.d.ts +3 -2
  220. package/dist-lib/lib/tools/skills/bundled-hash.js +6 -1
  221. package/dist-lib/lib/tools/skills/catalog.d.ts +3 -2
  222. package/dist-lib/lib/tools/skills/catalog.js +33 -28
  223. package/dist-lib/lib/tools/skills/cli-argv.d.ts +3 -2
  224. package/dist-lib/lib/tools/skills/cli-argv.js +55 -52
  225. package/dist-lib/lib/tools/skills/cli-schema-check.js +21 -10
  226. package/dist-lib/lib/tools/skills/cli-schema-load.js +28 -20
  227. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +6 -6
  228. package/dist-lib/lib/tools/skills/cli-schema.js +2 -1
  229. package/dist-lib/lib/tools/skills/credential-check.d.ts +6 -3
  230. package/dist-lib/lib/tools/skills/credential-check.js +24 -12
  231. package/dist-lib/lib/tools/skills/credential-grants.js +1 -6
  232. package/dist-lib/lib/tools/skills/credential-health.d.ts +5 -1
  233. package/dist-lib/lib/tools/skills/credential-health.js +5 -3
  234. package/dist-lib/lib/tools/skills/frontmatter-parser.js +10 -5
  235. package/dist-lib/lib/tools/skills/manifest-parse.js +84 -64
  236. package/dist-lib/lib/tools/skills/mention.js +7 -6
  237. package/dist-lib/lib/tools/skills/obsidian-readiness.d.ts +2 -2
  238. package/dist-lib/lib/tools/skills/obsidian-readiness.js +11 -3
  239. package/dist-lib/lib/tools/skills/overlay-crud.d.ts +6 -4
  240. package/dist-lib/lib/tools/skills/overlay-crud.js +12 -6
  241. package/dist-lib/lib/tools/skills/overlay-dir.js +4 -2
  242. package/dist-lib/lib/tools/skills/overlay-watch.d.ts +3 -2
  243. package/dist-lib/lib/tools/skills/overlay-watch.js +8 -4
  244. package/dist-lib/lib/tools/skills/setup-routes.js +1 -3
  245. package/dist-lib/lib/tools/skills/skill-settings.js +4 -1
  246. package/dist-lib/lib/tools/skills/skill-state.d.ts +6 -11
  247. package/dist-lib/lib/tools/skills/skill-state.js +48 -29
  248. package/dist-lib/lib/tools/skills/tool-grants.js +6 -3
  249. package/dist-lib/lib/tools/toolsets.js +2 -1
  250. package/dist-lib/lib/tools/translate.d.ts +4 -5
  251. package/dist-lib/lib/tools/translate.js +31 -14
  252. package/dist-lib/lib/tools/url-guard.js +2 -1
  253. package/dist-lib/lib/tools/weather-lookup/place.js +8 -4
  254. package/dist-lib/lib/tools/weather-lookup/report.d.ts +7 -1
  255. package/dist-lib/lib/tools/weather-lookup/report.js +46 -9
  256. package/dist-lib/lib/tools/weather-lookup.js +18 -12
  257. package/dist-lib/lib/tools/web-fetch.js +47 -24
  258. package/dist-lib/lib/tools/web-search.js +8 -3
  259. package/dist-lib/lib/validating-lookup.d.ts +4 -4
  260. package/dist-lib/lib/wav-encode.d.ts +1 -1
  261. package/dist-lib/lib/wav-encode.js +11 -10
  262. package/dist-lib/lib/web-fetch/embedded-state.js +24 -12
  263. package/dist-lib/lib/web-fetch/extract.js +4 -2
  264. package/dist-lib/lib/web-fetch/images.js +8 -4
  265. package/dist-lib/lib/web-fetch/video.d.ts +2 -2
  266. package/dist-lib/lib/web-fetch/video.js +8 -4
  267. package/dist-lib/lib/web-search/engine-registry.d.ts +2 -2
  268. package/dist-lib/lib/web-search/engines/aol.js +1 -1
  269. package/dist-lib/lib/web-search/engines/bing.js +1 -1
  270. package/dist-lib/lib/web-search/engines/brave.js +4 -1
  271. package/dist-lib/lib/web-search/engines/duckduckgo.js +3 -2
  272. package/dist-lib/lib/web-search/engines/google.js +7 -3
  273. package/dist-lib/lib/web-search/engines/wikipedia.js +5 -4
  274. package/dist-lib/lib/web-search/engines/yahoo.js +1 -1
  275. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -4
  276. package/dist-lib/lib/web-search/fetch-html.js +13 -7
  277. package/dist-lib/lib/web-search/orchestrator.d.ts +9 -5
  278. package/dist-lib/lib/web-search/orchestrator.js +3 -3
  279. package/dist-lib/lib/wire.d.ts +3 -2
  280. package/dist-lib/lib/wire.js +23 -15
  281. package/dist-lib/schema/baseline.js +10 -5
  282. package/dist-lib/schema/build.js +2 -1
  283. package/dist-lib/schema/harness.d.ts +15 -2
  284. package/dist-lib/schema/harness.js +39 -2
  285. package/dist-lib/schema/release-tags.d.ts +3 -2
  286. package/dist-lib/schema/release-tags.js +6 -3
  287. package/dist-lib/spec/bind.js +9 -0
  288. package/dist-lib/spec/calls.js +3 -0
  289. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  290. package/dist-lib/spec/hrpc/index.js +39 -3
  291. package/dist-lib/spec/hrpc/messages.js +105 -8
  292. package/dist-lib/spec/hyperschema/index.js +105 -8
  293. package/dist-lib/spec/hyperschema/schema.json +77 -1
  294. package/dist-lib/spec/hyperschema/types.d.ts +25 -1
  295. package/package.json +14 -14
  296. package/worker/harness-sidecar.mjs +15 -22
  297. package/worker/stow-entry.mjs +7 -4
  298. package/dist-lib/lib/tools/mcp/utils.d.ts +0 -47
  299. /package/dist-lib/lib/tools/http-request/{utils.d.ts → request.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -8,130 +8,406 @@ While the major version is `0`, a breaking change bumps the minor version.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.6.0] - 2026-09-28
12
+
13
+ ### Added
14
+
15
+ - `loadModel({ model, agent })` loads a downloaded chat, embedding, speech or
16
+ transcription model ahead of use, under the key a request for it would load
17
+ under, so the first request finds it loaded. `unloadModel({ model })` unloads
18
+ every copy of a model once the requests holding it finish, and says whether it
19
+ could. Image and music models load for each generation and are refused. A load
20
+ or an unload counts as a call in flight for `setModelIdleTimeout`, and one that
21
+ lands during an idle unload waits for it. Also on the wire as `load-model` and
22
+ `unload-model`. (#133)
23
+
24
+ ```js
25
+ const { loaded } = await harness.loadModel({ model: QWEN_35_4B, agent: { ctxSize: 8192 } })
26
+ const { unloaded } = await harness.unloadModel({ model: QWEN_35_4B })
27
+ ```
28
+
29
+ - A chat model loaded with `loadModel` is pinned: it does not count against
30
+ `completionModelCap`, so several stay loaded at once and a request for
31
+ another model leaves them in place. A manual load never unloads another model
32
+ to fit; it returns `{ loaded: false, reason: 'memory' }` when the SDK's
33
+ `assessModelFit` says it would not fit beside what is loaded. A request always
34
+ gets its model: pinned models no reply is using give way oldest first when it would not fit,
35
+ and a request or a manual load for a model under other settings replaces its idle copy. Image and
36
+ music generation and an idle unload still clear every idle chat model. (#134)
37
+
38
+ ```js
39
+ const { loaded, reason } = await harness.loadModel({ model: QWEN_35_9B })
40
+ if (reason === 'memory') showNotice('Unload a model first')
41
+ ```
42
+
43
+ ### Changed
44
+
45
+ - A model loading or unloading in any pool emits `model` with `catalogChanged`,
46
+ not only a chat model, so `list()` reports a fresh `isLoaded` for embedding,
47
+ speech and transcription models too. (#133)
48
+ - Speech, transcription and embedding requests key their pool entry by the
49
+ model's registry src however the caller names it, so a model named once by
50
+ its catalog name and once by its src is one copy in memory. (#133)
51
+
52
+ ### Fixed
53
+
54
+ - A chat model loads when the host passes no `llmDevice`. 0.5.0 sent `device`,
55
+ `gpu_layers`, `main-gpu` and `openclCacheDir` to the SDK as `undefined`,
56
+ which the SDK spreads over its defaults: with no device the load failed with
57
+ "must specify a device: 'gpu' or 'cpu'", and with a device but no
58
+ `llmGpuLayers` the default of 99 GPU layers was lost. Unset fields are left
59
+ out again. (#137)
60
+
61
+ ## [0.5.0] - 2026-09-25
62
+
63
+ ### Added
64
+
65
+ - `setModelIdleTimeout(ms)` frees memory once the device has been idle that
66
+ long: with no `completion`, `invoke`, `embed`, `transcribe`, `speak`,
67
+ `speakStream` or `warmModel` call in flight, every idle resident model is
68
+ unloaded, including the image, music and translation runtimes. The next call
69
+ loads what it needs again. `0`, the default, is Never, and arms no timer. A
70
+ turn waiting on a tool or an approval between rounds counts as busy, so it
71
+ keeps its model. Also on the wire as `set-model-idle-timeout`. (#131)
72
+
73
+ ```js
74
+ await harness.setModelIdleTimeout(30 * 60_000)
75
+ ```
76
+
77
+ - `setCustomModels` entries take an optional `label`, the name the user gave
78
+ the model, and `list()` rows carry it as `label`. The tool-call format is
79
+ matched against the label as well as the file name, so a custom model
80
+ registered under an opaque id still gets its family's format. (#124)
81
+
82
+ - `setCustomModels` entries take an optional `contextLength`, the model's
83
+ trained window. (#124)
84
+
85
+ ```js
86
+ await harness.setCustomModels([
87
+ {
88
+ name: 'custom-7f3a',
89
+ label: 'Gemma 4 E4B',
90
+ src: preview.src,
91
+ contextLength: preview.contextLength
92
+ }
93
+ ])
94
+ ```
95
+
96
+ ### Changed
97
+
98
+ - A custom model loads with a context of the smaller of 51,200 tokens and its
99
+ trained `contextLength`, instead of the default. An asked-for context size
100
+ still wins. (#124)
101
+
102
+ ### Fixed
103
+
104
+ - `previewModelSource` takes a direct link to one shard of a split `.gguf` as
105
+ the whole set: `src` is the first part, `sizeBytes` adds up every part, and
106
+ the label names the count, as in `model.gguf (3 parts)`. A missing part
107
+ refuses with `not-found` or `unreachable`. (#124)
108
+
109
+ - `isCached` reports a sharded custom download once every shard is on disk,
110
+ and `deleteModel` removes all of its shards. (#124)
111
+
11
112
  ## [0.4.0] - 2026-09-24
12
113
 
13
114
  ### Added
14
115
 
15
- - `Harness#previewModelSource({ src, mmprojSrc })`: checks a Hugging Face link,
16
- a direct `.gguf` link or a local path without downloading it. Returns the
17
- download link, size, context length, architecture, license, task type and a
18
- fit verdict, lists the files of a repo that holds several, or refuses with a
19
- reason. (#121)
20
- - `setCustomModels` takes `taskType` and `expectedSize`. Only `llm` custom
21
- models are registered, and a custom name never resolves for another task.
116
+ - `previewModelSource({ src, mmprojSrc })` checks a Hugging Face link, a direct
117
+ `.gguf` link or a local path before a host adds it as a custom model. It
118
+ reads the Hugging Face metadata and the file's GGUF header, never the whole
119
+ model. The answer is a file to register (`src`, `sizeBytes`,
120
+ `contextLength`, `architecture`, `sizeLabel`, `taskType`, `license`, `fit`
121
+ and `fitReasons`), a list of `files` to pick from for a repo that holds
122
+ several, or `ok: false` with a `refusal` code and a `reason` to show. It is
123
+ served on the `RemoteHarness` client and on an in-process `Harness`, and its
124
+ types are exported from the package root. (#121)
125
+
126
+ ```js
127
+ const preview = await harness.previewModelSource({ src: link })
128
+
129
+ if (!preview.ok) return showError(preview.reason)
130
+ if (!preview.src) return pickOneOf(preview.files) // then preview files[i].src
131
+ if (preview.taskType !== 'llm') return showError('Only an llm runs as a custom model')
132
+ ```
133
+
134
+ - `setCustomModels` entries take `taskType` and `expectedSize`, and `list()`
135
+ returns the registered custom models after the catalog rows, with
136
+ `registrySource: 'custom'`, `isLoaded`, and an `isCached` that is false
137
+ until every file is on disk and, when `expectedSize` is set, adds up to it.
138
+ (#121)
139
+
140
+ ```js
141
+ await harness.setCustomModels([
142
+ {
143
+ name: 'nanbeige-3b',
144
+ src: preview.src,
145
+ projectionSrc: preview.mmprojSrc,
146
+ taskType: preview.taskType,
147
+ expectedSize: preview.sizeBytes
148
+ }
149
+ ])
150
+
151
+ const models = await harness.list({ type: 'llm' })
152
+ const custom = models.filter((model) => model.registrySource === 'custom')
153
+ ```
154
+
155
+ - A `modelsDir` Harness option says where downloads live, so `list()` and
156
+ `deleteModel` can find a custom model's files. The spawned sidecar sets it
157
+ from its SDK storage; an in-process `Harness` has to pass it. (#121)
158
+
159
+ ```js
160
+ const harness = new Harness({ modelsDir })
161
+ ```
162
+
163
+ ### Changed
164
+
165
+ - `deleteModel(name)` removes the weight and projection a custom model
166
+ downloaded, where it used to remove nothing. It never touches a local path
167
+ the user pointed at, and the model stays registered, listed as not cached.
22
168
  (#121)
23
- - `list()` returns custom models with their task, size, and whether they are
24
- loaded and on disk; the new `modelsDir` option tells the harness where
25
- downloads live. (#121)
26
- - `Harness#deleteModel(name)` removes what a custom model downloaded, never a
27
- local path the user pointed at. (#121)
169
+
170
+ ```js
171
+ const { removed, freedBytes } = await harness.deleteModel('nanbeige-3b')
172
+ ```
173
+
174
+ - A custom model name resolves only where an `llm` is loaded, never for
175
+ another task, and a `setCustomModels` entry whose `taskType` is anything
176
+ other than `llm` is not registered. An entry without `taskType` is an `llm`,
177
+ as before. (#121)
28
178
 
29
179
  ### Fixed
30
180
 
31
- - A turn that preloads a skill with `@skill` no longer caches the inlined
32
- SKILL.md body under the chat's KV key. Each such turn left about 7k tokens
33
- stranded in the cache, until a later turn overflowed the context. (#120)
181
+ - A turn that preloads a skill with `@skill`, or names an active skill in its
182
+ prompt, no longer caches the inlined SKILL.md body under the chat's KV key.
183
+ Each such turn left about 7k tokens stranded in the cache, until a later turn
184
+ overflowed the context window and failed. (#120)
34
185
 
35
186
  ## [0.3.3] - 2026-09-24
36
187
 
37
188
  ### Changed
38
189
 
39
- - `@qvac/sdk` 0.20.0. Some addons now ship their prebuilds as per-platform
40
- packages (`@qvac/<addon>-<os>-<cpu>`, e.g. `audiogen-ggml` 0.4, `tts-ggml`
41
- 0.9), and `@qvac/model-fit` and `@qvac/fabric` join the native graph. A host
42
- that bundles the harness resolves prebuilds through each addon's
43
- `#host-addon` map, defers the new addons, and exempts the platform packages
44
- from any install age gate. (#85)
45
- - `@qvac/skills` 0.1.8: the diagrams skill carries every recipe inline, so a
46
- 2B no longer loops on a second `skill` load for a reference file it cannot
47
- name (QVAC-24436, QVAC-24502); the excel skill routes a chat image to create,
48
- not edit; the Google OAuth routes drop the loopback port. (#115)
190
+ - `@qvac/sdk` and `@qvac/inference` 0.20.0. Some addons now ship their
191
+ prebuilds as per-platform packages (`@qvac/<addon>-<os>-<cpu>`, for
192
+ `audiogen-ggml` 0.4 and `tts-ggml` 0.9), which the addon names in its
193
+ `#host-addon` import map, and `@qvac/model-fit` and `@qvac/fabric` join the
194
+ native graph as deferred addons. A host that packages the addons resolves
195
+ prebuilds through that map, and exempts the new package names from any
196
+ install age gate: bun skips a blocked optional dependency without a word,
197
+ and the prebuild goes missing. (#85)
49
198
  - A user message with several images reaches the SDK as one turn again. The
50
- per-image split added for QVAC-24156 is gone: SDK 0.20 carries the upstream
51
- mtmd fix (llama.cpp #27348), so equal-size still images no longer merge as
52
- video frames and abort the encode. `<key>-vision-history-v1` cache
53
- directories left by the split are no longer read; clearing all caches
199
+ per-image split added for QVAC-24156 is gone, since SDK 0.20 carries the
200
+ upstream fix (llama.cpp #27348) and equal-size still images no longer merge
201
+ as video frames and abort the encode. This release must not be paired with
202
+ SDK 0.19, where that crash comes back. `<key>-vision-history-v1` cache
203
+ directories left by the split are no longer read, and clearing all caches
54
204
  removes them. (#116)
205
+ - `@qvac/skills` 0.1.9. The diagrams skill carries every recipe inline, so a 2B
206
+ model no longer loops on loading a reference file it cannot name. The excel
207
+ skill routes a chat image to create rather than edit, the Google OAuth routes
208
+ drop the loopback port, and the Asana skill's first setup step names the
209
+ Asana MCP app to connect. (#115, #118)
55
210
 
56
211
  ### Fixed
57
212
 
58
213
  - Retrying or regenerating in a chat that held a multi-image message could
59
- reuse a stale cached prefix, because the split kept that chat's cache under a
60
- key the host never deletes. (#116)
61
- - `@qvac/skills` 0.1.9: the Asana skill's first setup step names the Asana MCP
62
- app to connect. (#118)
214
+ reuse a stale cached prefix and condition on the superseded answer. The split
215
+ kept that chat's cache under a sibling key the host's reclaim never reached.
216
+ (#116)
63
217
 
64
218
  ## [0.3.2] - 2026-09-23
65
219
 
66
220
  ### Changed
67
221
 
68
- - A networked python run's approval prompt lists every host its source names in
69
- a URL, or says it names none. On an approved run those hosts (and their `www.`
70
- form) are reached without asking. Any other host is asked about through
71
- `askUser` before it is resolved, and silence past a minute counts as a no. A
72
- host that does not wire `askUser` refuses every unnamed host. (#109)
222
+ - A networked Python run's approval label leads with the hosts its source names
223
+ in a URL, as `python · network: www.youtube.com …`, or says
224
+ `network: no host named in the code`. On an approved run those hosts, and
225
+ their `www.` form, are reached without asking; a subdomain is not. Any other
226
+ host, such as a CDN a library fans out to, a redirect or a URL built at
227
+ runtime, pauses the run and is asked about through the completion's
228
+ `askUser` before its name is looked up. A host allowed once stays allowed
229
+ for that run, and silence past a minute counts as a no. A host that does not
230
+ wire `askUser` refuses every unnamed host, and a skip-all run asks nothing.
231
+ (#109)
232
+
233
+ ```js
234
+ for await (const event of harness.completion({
235
+ model,
236
+ agent,
237
+ messages,
238
+ approve,
239
+ // options are ['Allow', 'Deny']; an answer starting with "Allow" allows it
240
+ askUser: ({ question, options }) => showQuestion(question, options)
241
+ })) {
242
+ render(event)
243
+ }
244
+ ```
73
245
 
74
246
  ### Fixed
75
247
 
76
- - A python call reached the network whenever the runtime allowed it, so an
77
- auto-run read call, such as a document build that loads an image from a URL,
78
- could reach the web without anyone approving it. The network now opens only to
79
- a call a person approved or that skip-all allowed. (#108)
80
- - A timed-out python run on a host with no worker thread spun forever. It is now
81
- interrupted at the deadline. (#104)
82
- - A timed-out python run on the worker could miss its SIGINT, which killed the
83
- worker along with the interpreter's state. The interrupt is resent until the
84
- run settles. (#112)
85
- - A python run that failed partway through a save attached the half-written
86
- file. A non-zero exit now attaches nothing and reports every declared output in
248
+ - A timed-out Python run spun at full CPU until the process was killed when
249
+ Pyodide had no worker thread, as for a stowed bundle whose host does not set
250
+ `QVAC_PYODIDE_WORKER_ENTRY`. It is now interrupted at the deadline, rejects
251
+ with `Command timed out after <ms>ms`, and the interpreter keeps its state. A
252
+ blocking call such as a long `time.sleep` still sees the interrupt only once
253
+ it returns. (#104)
254
+ - A timed-out Python run on the worker thread could miss its SIGINT, so the
255
+ worker was killed after the grace window along with the interpreter's state.
256
+ The interrupt is resent every 100 ms until the run settles. (#112)
257
+ - A Python run that failed partway through a save attached the half-written
258
+ file, and each failing retry attached another copy under the same name. A
259
+ non-zero exit now attaches nothing and reports every declared output in
87
260
  `missingOutputs`. (#110)
88
261
 
262
+ ### Security
263
+
264
+ - Python in `exec` reached the network whenever the runtime allowed it,
265
+ including a call that auto-ran as a read with no approval prompt, so a
266
+ document build that loads an image from a URL could reach the web without
267
+ anyone seeing it. The network now opens only to a call a person approved or
268
+ that skip-all let through, and sandbox mode stays offline. 0.3.1 is affected.
269
+ (#108)
270
+
89
271
  ## [0.3.1] - 2026-09-23
90
272
 
91
273
  ### Added
92
274
 
93
275
  - Python in `exec` reaches the network on Bare: `urllib`, `http.client` and
94
- `requests` go through the host's guarded fetch. (#94)
95
- - `SkillOAuthConfig.verify`: an optional probe a skill manifest declares, so a
96
- host can check a fresh token before saving it. It is carried over the wire.
97
- (#105)
98
- - A skill operation may name `response.decode` to turn a provider response into
99
- a compact shape, starting with `gmail-message`. (#69)
276
+ what is built on them, yt-dlp included, work unchanged, and `requests` works
277
+ through urllib3's emscripten backend. Every request goes through the guarded
278
+ fetch the web tools use, so each hop is checked before it is sent,
279
+ credentials do not follow a redirect to another origin, and a response is
280
+ capped at 100 MB. Where the global `fetch` is not the guarded one, as on
281
+ Node, Python stays offline, and sandbox mode keeps it offline everywhere. A
282
+ Python run may now declare `.mp4`, `.webm`, `.m4a` and `.mp3` outputs, each
283
+ checked by its file signature. (#94)
284
+ - `SkillOAuthConfig.verify`, parsed from a skill manifest's
285
+ `setup.routes[].oauth.verify` and carried over the wire, so a host can probe a
286
+ fresh token before saving it. A probe is an https `url` on the same site as
287
+ the route's `tokenUrl`, an optional `GET` or `POST` `method` and JSON `body`,
288
+ and `reject` rules. A rule matches a listed `status`, or a 2xx JSON body whose
289
+ `field` is not `notEqual`, and its `error` names the rejection. A probe that
290
+ breaks these rules is dropped and the route kept. `SkillOAuthVerify` and
291
+ `SkillOAuthVerifyRule` are exported. (#105)
292
+
293
+ ```js
294
+ const skills = await harness.skills()
295
+ const route = skills
296
+ .find((skill) => skill.name === 'asana')
297
+ ?.setup?.routes?.find((r) => r.kind === 'oauth')
298
+
299
+ // { url: 'https://mcp.asana.com/v2/mcp', method: 'POST',
300
+ // body: '{"jsonrpc":"2.0",…}',
301
+ // reject: [{ status: [401, 403], error: 'asana_not_mcp_app' }] }
302
+ const verify = route?.oauth?.verify
303
+ ```
304
+
305
+ - A skill operation may name `response.decode` to turn a provider's JSON body
306
+ into a compact shape before the model sees it, starting with
307
+ `gmail-message`: the ids and labels, the From, To, Subject, Date, Message-ID
308
+ and References headers, the snippet, and the body as text, capped at 4000
309
+ characters. A `gmail-get` request builder joins it. A manifest naming both
310
+ `pick` and `decode`, or a builder or decoder this harness does not have, is
311
+ rejected. (#69)
312
+
313
+ ### Changed
314
+
315
+ - A compiled skill operation whose HTTP method is `GET`, `HEAD` or `OPTIONS` is
316
+ a read: it auto-runs without an approval prompt, like the same request through
317
+ `http_request`, and takes no `intent` argument. MCP operations and every other
318
+ method are still writes that ask. (#69)
319
+ - `@qvac/skills` 0.1.7, now pinned exactly: Gmail reads a message with
320
+ `gmail_get` instead of telling the model to decode base64, which needs this
321
+ release's decoder, and Asana rejects a non-MCP app at connect time through
322
+ a `verify` probe. (#105)
100
323
 
101
324
  ### Fixed
102
325
 
103
326
  - A malformed `<tool_call>` left its JSON arguments in the reply. The whole span
104
- is now stripped. (#100)
327
+ is now stripped, and an unclosed one is stripped to the end of the text.
328
+ (#100)
105
329
  - A misspelt, placeholder or repeated `skill` load returned an error the model
106
- retried. It now resolves to the file that was meant, or to the already-loaded
107
- body. (#101)
108
- - When the model reached for `pip` or a video tool on the shell path, the
109
- redirect it got now names the call that works. (#71)
330
+ retried until the turn ran out. A placeholder or the skill's own name now
331
+ reads the body, a near miss of one of its files loads that file, and a repeat
332
+ is acknowledged without re-reading it. (#101)
333
+ - When the model reached for `pip` or a video tool on the shell path, it was
334
+ only told what the path is not, and retried the same call. Each refusal now
335
+ names the call that works, such as `packages: ["requests"]` with the pin a
336
+ vendored package installs offline under. A missing import names the package
337
+ that provides it, and `python -c`, a heredoc or bare Python source runs as
338
+ Python instead of being sent back for a `language` argument. (#71)
110
339
 
111
340
  ## [0.3.0] - 2026-09-23
112
341
 
113
342
  ### Added
114
343
 
115
- - `translate` capability: translates text without loading an LLM. The language
116
- pair picks the route before anything loads: the TranslatePsy-EuroNano packs for
117
- English and nine European languages, per-pair Bergamot models for the other 43,
118
- and TranslatePsy-AfriSLM for 16 languages Bergamot has no model for, named by
119
- ISO 639-1 or FLORES-200 code. An omitted `from` is detected from the text. (#76)
120
- - `translationPlanFor`, `NMT_TO_ENGLISH`, `NMT_FROM_ENGLISH`, `EURONANO_LANGUAGES`,
121
- `SLM_LANGUAGE_CODES` and `SLM_LANGUAGE_NAMES` exports, so a host can show a
122
- language picker or preview a route without a registry lookup (#76)
344
+ - A `translate` capability for `invoke`: it translates text without loading an
345
+ LLM. The language pair picks the route before anything loads. English and
346
+ one of nine European languages go through the TranslatePsy-EuroNano packs,
347
+ the other 43 languages Bergamot serves go through their per-pair models
348
+ (pivoting through English when neither side is English), and 16 languages
349
+ Bergamot has no model for go through TranslatePsy-AfriSLM, named by ISO 639-1
350
+ or FLORES-200 code. An omitted `from` is detected from the text, except for
351
+ the AfriSLM languages, which need it named. The `tool-result` carries the
352
+ text, the pair and the `engine` that answered. (#76)
353
+
354
+ ```js
355
+ for await (const event of harness.invoke({
356
+ capability: 'translate',
357
+ args: { text: 'The weather is nice today.', to: 'de' }
358
+ })) {
359
+ if (event.type === 'tool-result') show(event.result.text)
360
+ if (event.type === 'error') showError(event.message)
361
+ }
362
+ // result: { text, from, to: 'de', engine: 'nmt', pivoted: false }
363
+ ```
364
+
365
+ - `translationPlanFor`, `NMT_TO_ENGLISH`, `NMT_FROM_ENGLISH`,
366
+ `EURONANO_LANGUAGES`, `SLM_LANGUAGE_CODES` and `SLM_LANGUAGE_NAMES` exports,
367
+ so a host can build a language picker or preview a route without a registry
368
+ lookup. (#76)
369
+
370
+ ```js
371
+ import { translationPlanFor, SLM_LANGUAGE_NAMES } from '@qvac/harness'
372
+
373
+ const plan = translationPlanFor('es', 'ja')
374
+ // { route: 'nmt', from: 'es', to: 'ja',
375
+ // model: 'model.esen.intgemm', pivot: 'model.enja.intgemm' }
376
+ if ('error' in plan) showError(plan.error)
377
+ ```
378
+
123
379
  - `enhance: 'never' | 'auto'` on `invoke`. With `'auto'`, an image prompt is
124
- rewritten by a model already resident on the device and never triggers a load;
125
- `metrics` reports the `route` taken, the model used and the tokens the rewrite
126
- spent. Omitting `enhance` behaves as `'never'`. (#77)
380
+ rewritten by a model already resident on the device, and never triggers a
381
+ load: with nothing resident, or a rewrite that fails, the run goes direct.
382
+ `metrics` reports the `route` taken (`direct` or `enhanced`), the
383
+ `routeModel` used and the tokens the rewrite spent. Omitting `enhance`
384
+ behaves as `'never'`, which passes the arguments through untouched. (#77)
385
+
386
+ ```js
387
+ for await (const event of harness.invoke({
388
+ capability: 'image',
389
+ args: { prompt: 'funny meme' },
390
+ enhance: 'auto'
391
+ })) {
392
+ if (event.type === 'metrics') log(event.route, event.routeModel)
393
+ }
394
+ ```
395
+
396
+ ### Changed
397
+
398
+ - The bundle defers `@qvac/translation-nmtcpp` like the other inference
399
+ addons, so `prebuilds-manifest.json` now names its prebuilds and a host that
400
+ assembles them beside the bundle has to include it. It is already a
401
+ dependency of `@qvac/sdk`. (#76)
127
402
 
128
403
  ## [0.2.12] - 2026-09-22
129
404
 
130
405
  ### Fixed
131
406
 
132
- - Sandbox mode refused every `exec` call that installs packages, which broke the
133
- pdf, presentations, word and excel skills. Packages the harness vendors now
134
- install; one that would fall back to PyPI is still refused. (#91)
407
+ - Sandbox mode refused every `exec` call that installs packages, which broke
408
+ the pdf, presentations, word and excel skills. A request whose packages are
409
+ all vendored now installs from the shipped wheels; one that would fall back
410
+ to PyPI is still refused. (#91)
135
411
 
136
412
  ## [0.2.11] - 2026-09-22
137
413
 
@@ -139,161 +415,484 @@ Tagged but never published to npm; these changes first shipped in 0.2.12.
139
415
 
140
416
  ### Added
141
417
 
142
- - `Harness#invoke({ capability, args })` runs one capability on its own, with no
143
- model, dispatch or approval callback. It is a frame on the existing
144
- `completion` stream, with the same callbacks and events. (#74)
145
- - `Harness#detectLanguage({ text, topK })`, the `detect-language` hrpc method and
146
- its `RemoteHarness` client: language detection with no model to load (#75)
147
- - `SkillOAuthConfig` on `SkillSetupRoute.oauth`, parsed from a skill manifest's
148
- `setup.routes[].oauth` (#88)
418
+ - `invoke({ capability, args })` runs one capability on its own, with no
419
+ model, no dispatch and no approval callback: the caller already decided.
420
+ `image` is the first capability. It is a frame on the existing `completion`
421
+ stream, so it takes the same `attach`, `progress` and abort signal and emits
422
+ the same `tool-call`, `tool-result`, `metrics`, `error` and `aborted` events.
423
+ `metrics` reports `totalTokens: 0`. It is on `Harness` and `RemoteHarness`
424
+ alike, and optional on `HarnessContract`. (#74)
425
+
426
+ ```js
427
+ for await (const event of harness.invoke({
428
+ capability: 'image',
429
+ args: { prompt: 'a lighthouse at dusk', width: 768, height: 512 },
430
+ attach,
431
+ progress,
432
+ signal
433
+ })) {
434
+ if (event.type === 'tool-result') showImage(event.result)
435
+ if (event.type === 'error') showError(event.message)
436
+ }
437
+ ```
438
+
439
+ - `detectLanguage({ text, topK })`, served over hrpc as `detect-language` and
440
+ on `RemoteHarness`: language detection with no model to load. Without
441
+ `topK`, or with 1, it answers with the single best match and no
442
+ probability. Above that it returns ranked candidates with their
443
+ probabilities, best first, and may return fewer than asked. Text it cannot
444
+ place answers `und`. (#75)
445
+
446
+ ```js
447
+ const [best] = await harness.detectLanguage({ text })
448
+ // { code, language }
449
+
450
+ const ranked = await harness.detectLanguage({ text, topK: 3 })
451
+ // [{ code, language, probability }, ...]
452
+ ```
453
+
454
+ - `SkillOAuthConfig` on `SkillSetupRoute.oauth`, parsed from a skill
455
+ manifest's `setup.routes[].oauth` and carried over the wire, so a host can
456
+ run a skill's OAuth setup from its manifest instead of its own provider
457
+ table. A route missing `stateKey`, `authUrl`, `tokenUrl` or `tokenAuth`
458
+ carries no `oauth`. (#88)
459
+
460
+ ### Changed
461
+
462
+ - `@qvac/skills` 0.1.6: the eight OAuth skills declare their setup routes'
463
+ OAuth config, the Spotify skill always makes the play call after a search,
464
+ and the weather skill looks the place up with `weather_lookup`. (#88)
149
465
 
150
466
  ### Fixed
151
467
 
152
- - `web_fetch` offers file downloads (PDF, Office documents, images) in its
153
- description, and `attachDocument` returns the `attachmentId`, `fileName` and
154
- `byteLength` it creates, so a downloaded file survives a history fold (#89)
468
+ - Asked to fetch a file, the model tried `http_request` and `exec` before
469
+ `web_fetch`, and once the file was attached it had no id to read it with.
470
+ `web_fetch` now offers file downloads (PDF, Office documents, images) in its
471
+ description, and its result carries the `attachmentId`, `fileName` and
472
+ `byteLength` it created, so the model can stage the file into `exec` and it
473
+ survives a history fold. (#89)
155
474
 
156
475
  ## [0.2.10] - 2026-09-22
157
476
 
158
477
  ### Added
159
478
 
160
479
  - `sandboxMode` agent option: an agent with no network access. It withholds
161
- every tool that reaches the network, not just web search. (#73)
162
- - `@qvac/harness/office`: XLSX and PPTX text extraction for Knowledge beside the
163
- existing DOCX support, keeping sheets, column positions, cached formula values,
164
- slide text, grouped shapes and tables (#83)
480
+ every tool that reaches the network (the web search, HTTP request and MCP
481
+ toolsets, including skill operations over HTTP or MCP), not just web search.
482
+ `exec` stays, but refuses package installs and well-known network commands
483
+ such as `curl`, `git` and `npm`. That is a guard, not an OS-level sandbox.
484
+ (#73)
485
+
486
+ ```js
487
+ for await (const event of harness.completion({
488
+ model,
489
+ messages,
490
+ agent: { ...agent, sandboxMode: true }
491
+ })) {
492
+ render(event)
493
+ }
494
+ ```
495
+
496
+ - `@qvac/harness/office`: XLSX and PPTX text extraction for Knowledge beside
497
+ DOCX, through the same bundled Python worker. It keeps sheet names, column
498
+ positions, cached formula values, slide text, grouped shapes and tables;
499
+ speaker notes, images and charts are left out. The format comes from the
500
+ file extension, with `mimeType` as the fallback. `@qvac/harness/docx` is
501
+ unchanged. (#83)
502
+
503
+ ```js
504
+ import { extractOfficeText, closeOfficeWorker } from '@qvac/harness/office'
505
+
506
+ const text = await extractOfficeText(bytes, 'report.xlsx', { signal, mimeType })
507
+ // at host shutdown, once extractions have settled
508
+ await closeOfficeWorker()
509
+ ```
165
510
 
166
511
  ### Fixed
167
512
 
168
- - A tool call that already failed this turn with the same arguments is answered
169
- from its first result instead of running again, so a small model can no longer
170
- loop on the same failure (#78)
171
- - A guessed `auth.tokenCredentialKey` is resolved by URL instead of being refused
172
- with an error claiming the skill was not loaded (#72)
513
+ - A small model could loop on the same failing tool call, up to the round
514
+ cap. A call that already failed this turn with the same arguments is now
515
+ answered from its first result without running again, until a later round
516
+ succeeds. Three such answers end the turn's tool rounds. A failure that may
517
+ pass on retry, such as a timeout, a 5xx, 408 or 429, still runs again, and
518
+ the tools that can fail that way now say so in a `code` or `status` field on
519
+ their result. (#78)
520
+ - A guessed `auth.tokenCredentialKey` on `http_request` or `mcp_call` was
521
+ refused with an error claiming the skill was not loaded. It now resolves to
522
+ the one connected credential allowed for that URL, and when none is
523
+ connected the error says so and tells the user to connect it in Skills.
524
+ (#72)
173
525
 
174
526
  ## [0.2.9] - 2026-09-22
175
527
 
176
528
  ### Added
177
529
 
178
- - `captureInferenceCalls` harness option: emits `inference-call` and
179
- `inference-sent` events carrying the exact payload of every inference call (#51)
180
- - The `metrics` event carries the last round's `stopReason`, so a turn cut off by
181
- the context window is distinguishable from a complete one (#62)
530
+ - `captureInferenceCalls` on a completion's context: each round then emits an
531
+ `inference-call` event with the exact payload it is about to send (model,
532
+ messages, tools, generation params, cache key and prompt prefix), and an
533
+ `inference-sent` event once it ends, with the tokens it spent, its `outcome`
534
+ (`completed`, `overflowed` or `failed`) and, on the bundled patched SDK, the
535
+ slice of `messages` the KV cache let through. An attempt the overflow ladder
536
+ rejected is still recorded, with its `droppedTurns`. Off unless asked for,
537
+ since every record carries a whole prompt. The flag and both events are new on
538
+ the hrpc wire, so the host and the spawned harness both need 0.2.9. (#51)
539
+
540
+ ```js
541
+ for await (const event of harness.completion({ model, messages, captureInferenceCalls: true })) {
542
+ if (event.type === 'inference-call') record(event.call.round, event.call.messages)
543
+ if (event.type === 'inference-sent') settle(event.round, event.outcome, event.tokens)
544
+ }
545
+ ```
546
+
547
+ - The `metrics` event carries the last round's `stopReason`, and leaves it out
548
+ when the SDK reports none. A turn cut off by the context window ends cleanly
549
+ with `stopReason: 'length'`, so a host can tell it from a complete answer.
550
+ (#62)
551
+
552
+ ```js
553
+ if (event.type === 'metrics' && event.stopReason === 'length') markTruncated()
554
+ ```
182
555
 
183
556
  ### Changed
184
557
 
185
- - The published tarball no longer includes docs, agent files or eval output (#79)
558
+ - The published tarball no longer carries `docs/`, `CLAUDE.md`, `.claude/` or
559
+ eval output. (#79)
186
560
 
187
561
  ## [0.2.8] - 2026-09-17
188
562
 
189
563
  ### Added
190
564
 
191
- - `weather_lookup` tool: looks weather up and leads with the place the service
192
- actually resolved, refusing when it disagrees with the country or state asked
193
- for, instead of reporting the weather of wherever a nonexistent place landed
194
- (#52)
565
+ - `weather_lookup`, a tool that looks weather up on wttr.in by `location` and
566
+ leads its report with the place the service actually resolved. When that
567
+ place disagrees with the country or state the user named, it refuses instead
568
+ of reporting the weather of wherever the geocoder landed: a nonexistent town
569
+ no longer gets a forecast, and a bare "Rome" the service places in Togo says
570
+ so. It is offered only once the weather skill is loaded, needs no approval,
571
+ and the `http-request` toolset switch turns it off. The spawned sidecar
572
+ registers it; an in-process host adds it to its tools. (#52)
195
573
 
196
- ### Fixed
574
+ ```js
575
+ import { createWeatherLookupTool } from '@qvac/harness/lib/tools/weather-lookup.ts'
197
576
 
198
- - `@qvac/skills` 0.1.4: Apple Notes create-note no longer renames a note after
199
- creating it, which failed on iCloud accounts and left duplicates (#48)
577
+ const harness = new Harness({ tools: [...tools, createWeatherLookupTool()] })
578
+ ```
579
+
580
+ ### Changed
581
+
582
+ - **Breaking:** `currentReleaseLine` from `@qvac/harness/schema/release-tags`,
583
+ the schema build tooling, is now `currentReleaseVersion`, and returns the
584
+ full `bare-semver` `Version` rather than a `major.minor` string. A host that
585
+ imports it imports `currentReleaseVersion` instead and reads the line off
586
+ the version. (#55)
587
+
588
+ ```js
589
+ // 0.2.7
590
+ const line = currentReleaseLine(root) // '0.2'
591
+
592
+ // 0.2.8
593
+ const version = currentReleaseVersion(root) // Version 0.2.8
594
+ const line = `${version.major}.${version.minor}`
595
+ ```
596
+
597
+ - `previousReleaseTags` from the same module now counts the newest shipped tag
598
+ on the line being built as a baseline, not only older lines, so a schema
599
+ change is checked against the build users are running. Only tags newer than
600
+ the package version are left out. A host that regenerates its `spec/`
601
+ against it re-derives hyperschema's version counters once, so expect one
602
+ regeneration commit after upgrading. (#55, #58)
603
+ - `@qvac/skills` 0.1.4: the Apple Notes create-note operation no longer
604
+ renames a note after creating it, which failed on iCloud accounts and left
605
+ duplicate notes behind. (#48)
200
606
 
201
607
  ## [0.2.7] - 2026-09-16
202
608
 
203
609
  ### Added
204
610
 
205
- - `@qvac/harness/docx`: DOCX text extraction with the bundled Python runtime,
206
- preserving paragraph and table order (#46)
611
+ - `@qvac/harness/docx`: `extractDocxText` pulls a DOCX file's text for
612
+ Knowledge with the bundled Python runtime, keeping paragraphs and tables in
613
+ document order, one row per line with cells joined by `' | '`. It takes up to
614
+ 20 MiB and only the shipped wheels, never a network install. Set
615
+ `QVAC_PYODIDE_CACHE_DIR` to a writable directory first, so the worker seeds
616
+ its cache from those wheels and works offline. A bundled host also sets
617
+ `QVAC_PYODIDE_WORKER_ENTRY` and `QVAC_PYTHON_WHEELS_DIR` to its staged
618
+ worker and wheels. (#46)
619
+
620
+ ```js
621
+ import { extractDocxText, closeDocxWorker } from '@qvac/harness/docx'
622
+
623
+ const text = await extractDocxText(bytes, 'notes.docx', { signal })
624
+
625
+ // at shutdown, once outstanding extractions have settled
626
+ await closeDocxWorker()
627
+ ```
628
+
629
+ ### Fixed
630
+
631
+ - A Python run whose signal aborted while it waited for another run to finish
632
+ is no longer started. (#46)
207
633
 
208
634
  ## [0.2.6] - 2026-09-15
209
635
 
210
636
  ### Added
211
637
 
212
- - `completionModelCap` harness option, default 1: a completion model waits for a
213
- live run to release its model instead of loading beside it (#41)
214
- - `writeClient` and `writeCapabilities` take a `CallSiteOptions` argument whose
215
- `optionalRequests` names the requests the generated client may omit (#44)
638
+ - `completionModelCap`, default 1: how many completion models may be resident
639
+ at once. It is a `HarnessOptions` field, and rides the `llm` config through
640
+ `spawnHarness` and `harnessChild`. (#41)
641
+
642
+ ```js
643
+ const harness = spawnHarness({
644
+ entry,
645
+ sdkStorage,
646
+ llm: { contextSize: 8192, completionModelCap: 2 }
647
+ })
648
+ ```
649
+
650
+ - `writeClient` and `writeCapabilities` from `@qvac/harness/schema/generate`
651
+ take an options argument whose `optionalRequests` names the request types a
652
+ caller may omit: the generated client sends `{}` for them and the generated
653
+ capability types mark the input optional. Left out, only `@assistant/empty`
654
+ is optional, as before. A set you pass replaces that default, so include it
655
+ if you still want it. `writeClient` still takes an export name as a string.
656
+ (#44)
657
+
658
+ ```js
659
+ import { writeClient, writeCapabilities } from '@qvac/harness/schema/generate'
660
+
661
+ const optionalRequests = new Set(['@assistant/empty', '@assistant/mesh-scope'])
662
+ writeClient(builder, 'spec/calls.js', { exportName: 'engineClient', optionalRequests })
663
+ writeCapabilities(builder, 'spec/capabilities.d.ts', { optionalRequests })
664
+ ```
665
+
666
+ ### Changed
667
+
668
+ - A completion that needs a different model no longer loads it beside one a
669
+ live run is holding. It waits for that run to release its model, for up to
670
+ five minutes, and then loads anyway rather than failing. A `warmModel` call
671
+ waits two seconds at most, and skips the warm if no room was made. (#41)
216
672
 
217
673
  ## [0.2.5] - 2026-09-14
218
674
 
219
675
  ### Added
220
676
 
221
- - `HarnessOptions.gpuStats`: a host that already has the GPU reading can pass it
222
- and skip the probe (#36)
677
+ - A `gpuStats` option on `spawnHarness`, `harnessChild` and `HarnessOptions`.
678
+ A host that already has the GPU reading passes it, and the harness skips its
679
+ own probe. Left out, the harness probes for itself. (#36)
680
+
681
+ ```js
682
+ import { spawnHarness } from '@qvac/harness/lib/spawn.ts'
683
+
684
+ const harness = spawnHarness({
685
+ entry,
686
+ sdkStorage,
687
+ gpuStats: {
688
+ memory: { totalBytes: os.totalmem() },
689
+ gpu: { devices: [{ name: 'NVIDIA RTX 4070', videoMemoryBytes: 12884901888 }] }
690
+ }
691
+ })
692
+ ```
223
693
 
224
694
  ### Fixed
225
695
 
226
- - On a Linux snap the harness exited about a second after `open`, in a loop. GPUs
227
- are now enumerated in a short-lived child process. (#36)
228
- - On Windows, an image request after a presentation stuck on "Loading … into
229
- memory": Pyodide's boot no longer touches the process streams (#34)
696
+ - On a Linux snap the harness exited about a second after `open`, and the
697
+ supervisor respawned it in a loop: the boot-time capability probe created a
698
+ second GPU-info instance beside the SDK's own. On Linux and Windows under
699
+ Bare, GPUs are now enumerated in a short-lived child process, which gives up
700
+ after 5 seconds and reports no GPU. (#36)
701
+ - On Windows, asking for an image after a presentation left the chat stuck on
702
+ "Loading … into memory". Pyodide's boot opened the process streams from the
703
+ Python worker thread, which blocked the image-model loader's progress. Boot
704
+ now never touches them; a run's own output capture is unchanged. (#34)
230
705
 
231
706
  ## [0.2.4] - 2026-09-12
232
707
 
233
708
  ### Added
234
709
 
235
- - The sidecar entry is published, so a host can spawn the engine without the
236
- prebuilt bundle and with its native addons resolved at matching versions (#31)
710
+ - `@qvac/harness/worker/stow-entry.mjs`: the sidecar entry, published so a
711
+ host running from inside `node_modules` spawns the engine without the
712
+ prebuilt bundle. Every native addon then resolves beside the JS that calls
713
+ it, from one install. With the bundle, the addons come from the host's own
714
+ install, and a version skew there hangs the child on boot rather than
715
+ throwing: `@qvac/core` 0.1.2 shipped that way. The bundle stays published
716
+ for a host that relocates the code out of `node_modules`. (#31)
717
+
718
+ ```js
719
+ import { harnessChild } from '@qvac/harness/lib/spawn.ts'
720
+
721
+ const entry = new URL(import.meta.resolve('@qvac/harness/worker/stow-entry.mjs')).pathname
722
+ const spec = harnessChild(entry, { runner, skillsDir, wheelsDir, sdkStorage: storage.sdk })
723
+ ```
237
724
 
238
725
  ## [0.2.3] - 2026-09-12
239
726
 
727
+ ### Added
728
+
729
+ - `schema/generate` emits `writeStream` on the generated `HandlerFrames`, so a
730
+ duplex handler's frames type without a local widening. (#29)
731
+
732
+ ### Changed
733
+
734
+ - On Android and iOS, `resolveStorage` keeps models under the app's own
735
+ persistent root instead of the device HOME, which an app cannot write.
736
+ Desktop and Snap are unchanged, and `overrides.models` still wins. (#29)
737
+
738
+ ```js
739
+ import { resolveStorage } from '@qvac/harness/storage'
740
+
741
+ const storage = resolveStorage('prod')
742
+ // mobile: <persistent root>/.qvac/models
743
+ storage.sdk.models
744
+ ```
745
+
240
746
  ### Fixed
241
747
 
242
- - Every `./lib/tools/*` subpath is published. Only the 12 that `qvac-core` used
243
- were, so the mobile worklet's file tools could not resolve. (#29)
748
+ - Every `./lib/tools/*` subpath is published. The tarball listed only the ones
749
+ `qvac-core` imports, so the mobile worklet's file tools (`file-glob`, `grep`,
750
+ `read-files`, `create-documents`, `edit-documents`, `fs/scope`) failed with
751
+ `PACKAGE_PATH_NOT_EXPORTED`. 0.1.1 to 0.2.2 are affected. (#29)
244
752
 
245
753
  ## [0.2.2] - 2026-09-11
246
754
 
247
755
  ### Fixed
248
756
 
249
- - The lockfile no longer resolves a stale nested `@qvac/inference` (#27)
757
+ - A stale lockfile entry gave `@qvac/sdk` its own nested `@qvac/inference`
758
+ 0.19.1, beside the patched 0.19.0 the harness pins. The bundle is built from
759
+ the pinned copy alone again. (#27)
250
760
 
251
761
  ## [0.2.1] - 2026-09-11
252
762
 
253
763
  ### Fixed
254
764
 
255
- - The generated wire spec matches the schema it is built from (#25)
765
+ - The generated wire spec in 0.2.0 stamped the fields it added (`downloaded`
766
+ on a download event and as a model status, and `kvCacheKey`) with schema
767
+ version 7, the version 0.1.2 already shipped. They are version 8 now, so the
768
+ spec matches the schema it is built from. (#25)
256
769
 
257
770
  ## [0.2.0] - 2026-09-11
258
771
 
259
772
  ### Added
260
773
 
261
- - Chat compaction when the context window fills (#22)
262
- - Per-chat KV cache invalidation and reclaim (#22)
263
- - Notifications delivered from device-local state (#22)
264
- - A log of the tool prompt, and cheaper model-facing tool schemas (#22)
265
- - The target sample rate threaded through the Bare audio decoder (#22)
774
+ - `deleteKvCache(kvCacheKey)` deletes one chat's KV cache, every file kept
775
+ under that key, where `clearKvCache()` deletes them all. It is safe on a key
776
+ still in use: the next turn under it primes from scratch. It rides a new
777
+ `delete-kv-cache` hrpc route, and resolves with `{ status }` like
778
+ `clearKvCache`. (#22)
779
+
780
+ ```js
781
+ // the cacheKey the chat's completions ran under
782
+ const result = await harness.deleteKvCache(cacheKey)
783
+ if (result.status !== 'success') log(result.error)
784
+ ```
785
+
786
+ - A download's `complete` event carries `downloaded`, `true` when bytes were
787
+ fetched and `false` when the model was already on disk. An image model's
788
+ `model` events report `loading` rather than `downloading` when it is cached,
789
+ and a new `downloaded` status once a fetched one has loaded, so a host that
790
+ switches over `status` has one more case. (#22)
791
+
792
+ ```js
793
+ for await (const event of harness.download({ model })) {
794
+ if (event.type === 'complete' && event.downloaded) notifyDownloaded(model)
795
+ }
796
+
797
+ harness.on('model', ({ model, status }) => {
798
+ if (status === 'downloaded') notifyDownloaded(model)
799
+ })
800
+ ```
801
+
802
+ - `ToolDefinition.prompt`: an optional compact `description` and `parameters`
803
+ the model is shown in place of the full ones, which still validate the call.
804
+ `exec` and `skill` ship one, so every turn spends less of the prompt on
805
+ their schemas. (#22)
806
+ - `CONTEXT_OVERFLOW_MESSAGE` is exported, the message a turn fails with when
807
+ it does not fit the model's context window, so a host can tell that failure
808
+ apart and compact the chat. (#22)
809
+
810
+ ```js
811
+ import { CONTEXT_OVERFLOW_MESSAGE } from '@qvac/harness'
812
+ ```
813
+
814
+ - With `QVAC_HARNESS_LOG=1`, each turn logs `sdk tool definitions`: the tool
815
+ schemas it sent, largest first, with each one's size and the skill that
816
+ granted it. (#22)
266
817
 
267
818
  ### Changed
268
819
 
269
- - QVAC SDK 0.19: `@qvac/inference` serves the Bare condition behind a new `#sdk`
270
- import alias (#22)
271
- - `bare-http1` is pinned to 4.5.9 (#24)
820
+ - `@qvac/sdk` 0.19.0, pinned exactly. Under Bare the SDK is now served by
821
+ `@qvac/inference` 0.19.0, through a new `#sdk` import alias.
822
+ `@qvac/audiogen-ggml` joins the deferred addons, so `prebuilds-manifest.json`
823
+ names it and the host provides it too. (#22)
824
+ - Image and music generation schemas reach the model once their skill's
825
+ instructions are loaded, not as soon as the skill is enabled. Loading the
826
+ skill unlocks them within the same turn, at the cost of one cache re-prime
827
+ on that round. (#22)
828
+ - `@qvac/skills` 0.1.3: the weather skill keeps the place exactly as the user
829
+ wrote it, asks back on an ambiguous city name, and reports a failed lookup
830
+ instead of a temperature it never received. (#22)
831
+ - `bare-http1` is pinned to 4.5.9, as a dependency and as an override.
832
+ (#22, #24)
272
833
 
273
834
  ## [0.1.2] - 2026-09-11
274
835
 
275
836
  ### Fixed
276
837
 
277
- - `@qvac/harness/sdk-storage` and `#sdk-config-loader` failed to resolve under
278
- Bare: the tarball carried only the Node loader (#20)
838
+ - `@qvac/harness/sdk-storage` and `@qvac/harness/sdk-config-loader` failed to
839
+ resolve under Bare with `MODULE_NOT_FOUND`: the tarball carried only the Node
840
+ loader behind `#sdk-config-loader`, so `configureSdkStorage()` could not run
841
+ on Bare. Both loaders ship now. 0.1.1 is affected. (#20)
842
+
843
+ ```js
844
+ // Bare: loads again, nothing else changes
845
+ import { configureSdkStorage } from '@qvac/harness/sdk-storage'
846
+ ```
279
847
 
280
848
  ## [0.1.1] - 2026-09-10
281
849
 
282
850
  ### Added
283
851
 
284
- - A compiled library surface (`dist-lib/`, JavaScript and declarations), so the
285
- package's exports import without shipping TypeScript source (#18)
852
+ - The library surface ships compiled: `dist-lib/` carries JavaScript and a
853
+ `.d.ts` for every export, so `@qvac/harness` and its subpaths import from
854
+ npm under plain Node or Bare, with no TypeScript source or type-stripping
855
+ loader. Export keys are unchanged, `.ts`-suffixed deep paths included, so a
856
+ host that took the library from the repository switches to the package
857
+ without editing a specifier. Of `./lib/tools/*`, only the paths `qvac-core`
858
+ imports are published. (#18)
859
+
860
+ ```js
861
+ import { harnessChild, spawnHarness } from '@qvac/harness/lib/spawn.ts'
862
+ import { AbortedError, raceAbort } from '@qvac/harness/race-abort'
863
+ ```
286
864
 
287
865
  ## [0.1.0] - 2026-09-10
288
866
 
289
867
  ### Added
290
868
 
291
- - First release of the QVAC engine as `@qvac/harness`: the prebuilt
292
- `harness.bundle` a host spawns, its runtime content (skills from `@qvac/skills`
293
- and the hash-pinned Python wheels), and `prebuilds-manifest.json` naming the
294
- native addons it defers (#13, #15)
295
-
296
- [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.4.0...HEAD
869
+ - First release of the QVAC engine as `@qvac/harness`. The tarball is the
870
+ prebuilt `dist/harness.bundle`, one bundle for every platform, and the
871
+ generated `dist/harness.cjs` a host requires to spawn it as a bare-sidecar
872
+ process. It spawns and does not import: the export map points at library
873
+ source the tarball leaves out, so a host takes `spawnHarness`,
874
+ `harnessChild` and the transport from the repository until a compiled
875
+ surface ships. (#13)
876
+
877
+ ```js
878
+ const harness = require('@qvac/harness/dist/harness.cjs')
879
+
880
+ const { ipc, sidecar, exit } = await harness.start(args)
881
+ ```
882
+
883
+ - `dist/prebuilds-manifest.json` names every native prebuild directory the
884
+ bundle defers, keyed by its path relative to the bundle. The host assembles
885
+ those beside the bundle from its own `node_modules`, and installs the
886
+ `@qvac` inference addons and `pyodide` alongside. (#13)
887
+ - Skills come from `@qvac/skills` 0.1.2, a content-addressed bundle the
888
+ harness materializes at runtime. The Python wheels the tool runtime installs
889
+ ship in `wheels/`, each pinned by hash in `wheels/manifest.json`. (#13, #15)
890
+ - GPU stats are read through `bare-gpu-info`, so every dependency installs from
891
+ the public npm registry. (#12)
892
+
893
+ [Unreleased]: https://github.com/tetherto/qvac-harness/compare/v0.6.0...HEAD
894
+ [0.6.0]: https://github.com/tetherto/qvac-harness/compare/v0.5.0...v0.6.0
895
+ [0.5.0]: https://github.com/tetherto/qvac-harness/compare/v0.4.0...v0.5.0
297
896
  [0.4.0]: https://github.com/tetherto/qvac-harness/compare/v0.3.3...v0.4.0
298
897
  [0.3.3]: https://github.com/tetherto/qvac-harness/compare/v0.3.2...v0.3.3
299
898
  [0.3.2]: https://github.com/tetherto/qvac-harness/compare/v0.3.1...v0.3.2