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