@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/dist-lib/index.js CHANGED
@@ -8,6 +8,7 @@ import { closePythonWorker } from './lib/tools/exec/adapters/python/worker/clien
8
8
  import { completion, imageSettingsFrom } from './lib/capabilities/completion.js';
9
9
  import { detectLanguage } from './lib/capabilities/detect-language.js';
10
10
  import { invoke } from './lib/capabilities/invoke.js';
11
+ import { previewModelSource } from './lib/capabilities/preview-model-source.js';
11
12
  import { expandImagePrompt } from './lib/capabilities/enhance.js';
12
13
  import { cleanAnswer } from './lib/helpers/clean-answer.js';
13
14
  import { isTranslationModel } from './lib/models/nmt-catalog.js';
@@ -21,10 +22,11 @@ import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnc
21
22
  import { pushQueue } from './lib/queue.js';
22
23
  import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
23
24
  import { audioGenDownloadMetadata, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, infraModelsFor, modelNameToCapabilityProfile, musicModelSet, setCustomModels } from './lib/models/model-catalog.js';
24
- import { createModelStatusCache, invalidateModelStatusCache, listModels } from './lib/models/model-list.js';
25
+ import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
25
26
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
26
27
  import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, modelKey, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
27
28
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
29
+ import { createIdleReaper } from './lib/models/idle-unload.js';
28
30
  import { openclCacheDir } from './lib/models/opencl-cache.js';
29
31
  import { assertEmbedDevice, createEmbedConfig } from './lib/models/embed-config.js';
30
32
  import { createGpuFaultState, isSdkGpuDeviceLostError, isSdkModelWedgedError } from './lib/models/sdk-faults.js';
@@ -38,6 +40,7 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
38
40
  import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
39
41
  import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
40
42
  import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
43
+ import { errorMessage } from './lib/guards.js';
41
44
  import { hlog } from './lib/log.js';
42
45
  import { harness } from './spec/index.js';
43
46
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
@@ -104,6 +107,7 @@ export class Harness extends ReadyResource {
104
107
  _voiceLane = createVoiceLane();
105
108
  _state;
106
109
  _skillsDir;
110
+ _modelsDir;
107
111
  _overlayWatcher = null;
108
112
  _supportedSkills;
109
113
  _projectionModel;
@@ -131,7 +135,8 @@ export class Harness extends ReadyResource {
131
135
  _capability = null;
132
136
  _capabilityInflight = null;
133
137
  _modelFaults = new Map();
134
- constructor({ tools = [], sdkLoader, skillsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
138
+ _idle;
139
+ constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
135
140
  super();
136
141
  this._sdk = sdkLoader ?? createSdkLoader();
137
142
  this._llmDevice = llmDevice;
@@ -175,8 +180,9 @@ export class Harness extends ReadyResource {
175
180
  },
176
181
  onLoaded: (model) => {
177
182
  const transfer = imageTransfers.get(model);
178
- if (!transfer)
183
+ if (!transfer) {
179
184
  return;
185
+ }
180
186
  imageTransfers.delete(model);
181
187
  invalidateModelStatusCache(this._modelStatus);
182
188
  this.emit('model', { ...transfer, status: 'downloaded' });
@@ -184,9 +190,14 @@ export class Harness extends ReadyResource {
184
190
  onBeforeLoad: async (signal) => {
185
191
  if (this._models.destroyed)
186
192
  return;
187
- const evicted = await awaitModelCapacity(this._models, 1, DIFFUSION_EVICT_WAIT_MS, undefined, signal);
188
- if (signal?.aborted)
193
+ const evicted = await awaitModelCapacity(this._models, {
194
+ cap: 1,
195
+ deadlineMs: DIFFUSION_EVICT_WAIT_MS,
196
+ signal
197
+ });
198
+ if (signal?.aborted) {
189
199
  throw new AbortedError();
200
+ }
190
201
  if (!evicted) {
191
202
  throw new Error('the chat model is busy; try the image again once the current reply finishes');
192
203
  }
@@ -203,9 +214,14 @@ export class Harness extends ReadyResource {
203
214
  onBeforeLoad: async (signal) => {
204
215
  if (this._models.destroyed)
205
216
  return;
206
- const evicted = await awaitModelCapacity(this._models, 1, MUSIC_EVICT_WAIT_MS, undefined, signal);
207
- if (signal?.aborted)
217
+ const evicted = await awaitModelCapacity(this._models, {
218
+ cap: 1,
219
+ deadlineMs: MUSIC_EVICT_WAIT_MS,
220
+ signal
221
+ });
222
+ if (signal?.aborted) {
208
223
  throw new AbortedError();
224
+ }
209
225
  if (!evicted) {
210
226
  throw new Error('the chat model is busy; try the music again once the current reply finishes');
211
227
  }
@@ -235,6 +251,7 @@ export class Harness extends ReadyResource {
235
251
  this._projectionModel = projectionModel ?? defaultProjectionMap();
236
252
  this._state = HarnessState.RESUME;
237
253
  this._skillsDir = skillsDir;
254
+ this._modelsDir = modelsDir;
238
255
  this._supportedSkills = [];
239
256
  this._allSkills = [];
240
257
  this._credentialStore = credentialStore;
@@ -242,13 +259,18 @@ export class Harness extends ReadyResource {
242
259
  this._diffusionSdk = null;
243
260
  this._musicSdk = null;
244
261
  this._translationSdk = null;
245
- }
246
- async _open() {
247
- installResolutionDetector(import.meta);
248
262
  this._models = this._createPool();
249
263
  this._embedModels = this._createEmbedPool();
250
264
  this._ttsModels = this._createTtsPool();
251
265
  this._transcriptionModels = this._createTranscriptionPool();
266
+ this._idle = createIdleReaper({
267
+ sweep: () => this._unloadIdleModels(),
268
+ waitMs: WEDGED_EVICT_WAIT_MS,
269
+ onError: (err) => hlog('idle unload failed', { err: errorMessage(err) })
270
+ });
271
+ }
272
+ async _open() {
273
+ installResolutionDetector(import.meta);
252
274
  await this._reloadSkills();
253
275
  await this._startOverlayWatch();
254
276
  hlog('open', { skills: this._supportedSkills.length, tools: this._tools.length });
@@ -266,10 +288,7 @@ export class Harness extends ReadyResource {
266
288
  if (!token)
267
289
  continue;
268
290
  const storeKey = scopeKey({ agentId, skill, key });
269
- const record = {
270
- token,
271
- ...(typeof expiresAt === 'number' && expiresAt > 0 ? { expiresAt } : {})
272
- };
291
+ const record = typeof expiresAt === 'number' && expiresAt > 0 ? { token, expiresAt } : { token };
273
292
  const current = previous.get(storeKey);
274
293
  if (!current || current.token !== record.token || current.expiresAt !== record.expiresAt) {
275
294
  resetCredentialHealthAfterSnapshot(key, agentId, skill);
@@ -307,48 +326,57 @@ export class Harness extends ReadyResource {
307
326
  }
308
327
  async writeSkillFile(name, content) {
309
328
  const overlay = await ensureSkillsOverlayDir();
310
- if (!overlay)
329
+ if (!overlay) {
311
330
  throw new Error('no writable skills overlay is configured');
331
+ }
312
332
  await writeSkillFile(overlay, name, content);
313
333
  await this._reloadSkills();
314
334
  return { ok: true };
315
335
  }
316
336
  async copyBaseForEdit(name) {
317
337
  const overlay = await ensureSkillsOverlayDir();
318
- if (!overlay)
338
+ if (!overlay) {
319
339
  throw new Error('no writable skills overlay is configured');
340
+ }
320
341
  const result = await copyBaseForEdit(await this._baseSkillsDir(), overlay, name);
321
- if (result.copied)
342
+ if (result.copied) {
322
343
  await this._reloadSkills();
344
+ }
323
345
  return result;
324
346
  }
325
347
  async deleteOverlaySkill(name) {
326
348
  const overlay = await ensureSkillsOverlayDir();
327
- if (!overlay)
349
+ if (!overlay) {
328
350
  throw new Error('no writable skills overlay is configured');
351
+ }
329
352
  const result = await deleteOverlaySkill(overlay, name);
330
- if (result.removed)
353
+ if (result.removed) {
331
354
  await this._reloadSkills();
355
+ }
332
356
  return result;
333
357
  }
334
358
  resolveCredential(key, ctx) {
335
- if (this._credentialStore)
359
+ if (this._credentialStore) {
336
360
  return this._credentialStore(key, ctx);
361
+ }
337
362
  const agentId = ctx?.agentId ?? null;
338
363
  const deviceShared = isDeviceSharedCredentialKey(key);
339
364
  for (const skill of ctx?.skills ?? []) {
340
365
  const owned = this._credentials.get(scopeKey({ agentId, skill, key })) ??
341
366
  (agentId && deviceShared ? this._credentials.get(scopeKey({ skill, key })) : undefined);
342
- if (owned)
367
+ if (owned) {
343
368
  return owned;
369
+ }
344
370
  }
345
371
  if (agentId) {
346
372
  const scoped = this._credentials.get(scopeKey({ agentId, key }));
347
- if (scoped)
373
+ if (scoped) {
348
374
  return scoped;
375
+ }
349
376
  }
350
- if (!agentId || deviceShared)
377
+ if (!agentId || deviceShared) {
351
378
  return this._credentials.get(scopeKey({ key })) ?? null;
379
+ }
352
380
  return null;
353
381
  }
354
382
  async _completionSkills(agentId, model) {
@@ -359,7 +387,8 @@ export class Harness extends ReadyResource {
359
387
  }));
360
388
  }
361
389
  async skills(agentId, modelName, options) {
362
- await this.ready();
390
+ if (!this.opened)
391
+ await this.ready();
363
392
  const platform = this._platform ?? detectPlatform();
364
393
  const refresh = options?.refresh === true;
365
394
  return resolveSkillStatuses(this._allSkills, {
@@ -373,7 +402,7 @@ export class Harness extends ReadyResource {
373
402
  : await this._probeBinVersions(platform, refresh),
374
403
  refresh,
375
404
  skipCredentialProbe: options?.skipCredentialProbe === true,
376
- ...(modelName ? { activeProfile: modelNameToCapabilityProfile(modelName) } : {})
405
+ activeProfile: modelName ? modelNameToCapabilityProfile(modelName) : undefined
377
406
  });
378
407
  }
379
408
  async _reloadSkills() {
@@ -388,14 +417,15 @@ export class Harness extends ReadyResource {
388
417
  return;
389
418
  try {
390
419
  const overlay = await ensureSkillsOverlayDir();
391
- if (!overlay)
420
+ if (!overlay) {
392
421
  return;
422
+ }
393
423
  this._overlayWatcher = watchSkillsOverlay(overlay, () => {
394
- this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: err instanceof Error ? err.message : String(err) }));
424
+ this._reloadSkills().catch((err) => hlog('overlay reload failed', { err: errorMessage(err) }));
395
425
  });
396
426
  }
397
427
  catch (err) {
398
- hlog('overlay watch unavailable', { err: err instanceof Error ? err.message : String(err) });
428
+ hlog('overlay watch unavailable', { err: errorMessage(err) });
399
429
  }
400
430
  }
401
431
  async tools() {
@@ -408,8 +438,9 @@ export class Harness extends ReadyResource {
408
438
  if (!refresh && this._binVersionCache && this._binVersionCache.expiresAt > now) {
409
439
  return this._binVersionCache.bySkill;
410
440
  }
411
- if (this._binVersionInflight)
441
+ if (this._binVersionInflight) {
412
442
  return this._binVersionInflight;
443
+ }
413
444
  this._binVersionInflight = this._runBinVersionProbes(platform)
414
445
  .then((bySkill) => {
415
446
  this._binVersionCache = {
@@ -424,12 +455,14 @@ export class Harness extends ReadyResource {
424
455
  return this._binVersionInflight;
425
456
  }
426
457
  async _runBinVersionProbes(platform) {
427
- if (process.env.QVAC_NO_SKILL_BIN_PROBE)
458
+ if (process.env.QVAC_NO_SKILL_BIN_PROBE) {
428
459
  return new Map();
460
+ }
429
461
  const installed = new Set(this._tools.map((tool) => tool.name));
430
462
  const candidates = versionProbeSkills(this._allSkills, installed, platform);
431
- if (candidates.length === 0)
463
+ if (candidates.length === 0) {
432
464
  return new Map();
465
+ }
433
466
  let runtime;
434
467
  try {
435
468
  runtime = await bareRuntime();
@@ -449,8 +482,9 @@ export class Harness extends ReadyResource {
449
482
  const bySkill = new Map();
450
483
  for (const skill of candidates) {
451
484
  const states = await probeBinVersions(skill.requires, { platform, run });
452
- if (states.size > 0)
485
+ if (states.size > 0) {
453
486
  bySkill.set(skill.name, states);
487
+ }
454
488
  }
455
489
  return bySkill;
456
490
  }
@@ -469,17 +503,37 @@ export class Harness extends ReadyResource {
469
503
  }
470
504
  _projectionFor(model) {
471
505
  const custom = this._customProjection[model];
472
- if (custom)
506
+ if (custom) {
473
507
  return custom;
508
+ }
474
509
  const projection = this._projectionModel;
475
- if (!projection)
510
+ if (!projection) {
476
511
  return undefined;
512
+ }
477
513
  return typeof projection === 'string' ? projection : projection[model];
478
514
  }
479
515
  setCustomModels(entries) {
480
516
  setCustomModels(entries);
481
517
  this._customProjection = customProjectionMap();
482
518
  }
519
+ setModelIdleTimeout(ms) {
520
+ this._idle.configure(ms);
521
+ }
522
+ previewModelSource(context) {
523
+ return previewModelSource(context, {
524
+ assessFit: async ({ sha256Checksum, name, contextTokens }) => {
525
+ const sdk = await this._sdk.load();
526
+ const result = await sdk.assessModelFit({
527
+ models: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
528
+ });
529
+ const model = result.models[0];
530
+ return {
531
+ fit: model?.verdict ?? result.verdict,
532
+ fitReasons: [...(model?.reasons ?? result.reasons)]
533
+ };
534
+ }
535
+ });
536
+ }
483
537
  async describe() {
484
538
  if (!this.opened)
485
539
  await this.ready();
@@ -501,11 +555,12 @@ export class Harness extends ReadyResource {
501
555
  };
502
556
  }
503
557
  _loadedModels() {
504
- const pools = [this._embedModels, ...this._retiredEmbedModels];
505
- if (this._ttsModels)
506
- pools.push(this._ttsModels);
507
- if (this._transcriptionModels)
508
- pools.push(this._transcriptionModels);
558
+ const pools = [
559
+ this._embedModels,
560
+ ...this._retiredEmbedModels,
561
+ this._ttsModels,
562
+ this._transcriptionModels
563
+ ];
509
564
  return [...loadedModels(this._models), ...pools.flatMap((pool) => loadedModels(pool))];
510
565
  }
511
566
  _loadPluginSdk(current, store, importPlugin) {
@@ -539,8 +594,9 @@ export class Harness extends ReadyResource {
539
594
  const loadOpts = resolveLoadOptions({}, this._deviceConfig());
540
595
  const key = modelKey(model, loadOpts);
541
596
  const pool = this._models;
542
- if (!isResident(pool, key))
597
+ if (!isResident(pool, key)) {
543
598
  await this._makeRoomForCompletion(signal);
599
+ }
544
600
  try {
545
601
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
546
602
  const sdk = await this._sdk.load();
@@ -574,21 +630,40 @@ export class Harness extends ReadyResource {
574
630
  }
575
631
  _createPool() {
576
632
  const loadSdk = this._sdk.load;
577
- return createModelPool(loadSdk, (state) => this._notifyModel(state, true), this._deviceConfig(), this._loads);
633
+ return createModelPool(loadSdk, (state) => this._notifyModel(state, { languageModel: true }), {
634
+ deviceConfig: this._deviceConfig(),
635
+ loads: this._loads
636
+ });
578
637
  }
579
638
  _createEmbedPool() {
580
639
  const loadSdk = () => this._sdk.loadEmbed();
581
- return createEmbedPool(loadSdk, (state) => this._notifyModel(state, false), this._embedFaults, this._embedBatchSize, openclCacheDir, this._loads);
640
+ return createEmbedPool(loadSdk, (state) => this._notifyModel(state), {
641
+ faults: this._embedFaults,
642
+ batchSize: this._embedBatchSize,
643
+ openclCacheDir,
644
+ loads: this._loads
645
+ });
582
646
  }
583
647
  _createTtsPool() {
584
648
  const loadSdk = () => this._sdk.loadVoice();
585
- return createVoicePool(loadSdk, (state) => this._notifyModel(state, false), 'tts-ggml', 'tts', TTS_MODEL_CONFIG, undefined, this._loads);
649
+ return createVoicePool(loadSdk, (state) => this._notifyModel(state), {
650
+ modelType: 'tts-ggml',
651
+ addon: 'tts',
652
+ modelConfig: TTS_MODEL_CONFIG,
653
+ loads: this._loads
654
+ });
586
655
  }
587
656
  _createTranscriptionPool() {
588
657
  const loadSdk = () => this._sdk.loadVoice();
589
- return createVoicePool(loadSdk, (state) => this._notifyModel({ ...state, model: TRANSCRIPTION_MODEL_LABEL }, false), 'parakeet-transcription', 'parakeet', { useGPU: this._transcriptionDevice !== 'cpu' }, openclCacheDir, this._loads);
658
+ return createVoicePool(loadSdk, (state) => this._notifyModel({ ...state, model: TRANSCRIPTION_MODEL_LABEL }), {
659
+ modelType: 'parakeet-transcription',
660
+ addon: 'parakeet',
661
+ modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
662
+ openclCacheDir,
663
+ loads: this._loads
664
+ });
590
665
  }
591
- _notifyModel(state, languageModel) {
666
+ _notifyModel(state, { languageModel = false } = {}) {
592
667
  const catalogChanged = languageModel && (state.status === 'ready' || state.status === 'unloaded');
593
668
  if (catalogChanged) {
594
669
  invalidateModelStatusCache(this._modelStatus);
@@ -628,14 +703,17 @@ export class Harness extends ReadyResource {
628
703
  return this._enqueueEmbedControl(() => this._setEmbeddingDevice(device));
629
704
  }
630
705
  async _setEmbeddingDevice(device) {
631
- await this.ready();
706
+ if (!this.opened)
707
+ await this.ready();
632
708
  if (this.closing)
633
709
  throw new Error('harness is closing');
634
- if (this._embedFaults.device === device)
710
+ if (this._embedFaults.device === device) {
635
711
  return;
712
+ }
636
713
  this._embedFaults = createGpuFaultState({ device, gpuLayers: this._embedGpuLayers });
637
- if (this.suspended)
714
+ if (this.suspended) {
638
715
  return;
716
+ }
639
717
  const retired = this._embedModels;
640
718
  this._retiredEmbedModels.add(retired);
641
719
  this._embedModels = this._createEmbedPool();
@@ -649,15 +727,18 @@ export class Harness extends ReadyResource {
649
727
  }
650
728
  async suspend() {
651
729
  hlog('suspend called');
652
- if (this._state === HarnessState.SUSPEND)
730
+ if (this._state === HarnessState.SUSPEND) {
653
731
  return;
654
- if (this.closing)
732
+ }
733
+ if (this.closing) {
655
734
  return;
735
+ }
656
736
  this._state = HarnessState.SUSPEND;
657
737
  hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
658
738
  const pending = this._sdk.peek();
659
- if (pending)
739
+ if (pending) {
660
740
  this._loads.drain(await pending);
741
+ }
661
742
  await this._releaseRuntimes();
662
743
  await this._sdkLifecycle('suspend');
663
744
  invalidateModelStatusCache(this._modelStatus);
@@ -665,18 +746,20 @@ export class Harness extends ReadyResource {
665
746
  }
666
747
  async _sdkLifecycle(op) {
667
748
  const sdk = await this._sdk.peek()?.catch(noop);
668
- if (!sdk)
749
+ if (!sdk) {
669
750
  return;
751
+ }
670
752
  try {
671
753
  await sdk[op]();
672
754
  }
673
755
  catch (err) {
674
- hlog(`sdk ${op} failed`, { err: err instanceof Error ? err.message : String(err) });
675
- if (op === 'suspend')
756
+ hlog(`sdk ${op} failed`, { err: errorMessage(err) });
757
+ if (op === 'suspend') {
676
758
  return;
759
+ }
677
760
  await sdk.resume().catch((retryErr) => {
678
761
  hlog('sdk resume retry failed', {
679
- err: retryErr instanceof Error ? retryErr.message : String(retryErr)
762
+ err: errorMessage(retryErr)
680
763
  });
681
764
  });
682
765
  }
@@ -695,15 +778,38 @@ export class Harness extends ReadyResource {
695
778
  if (outcome.status !== 'rejected')
696
779
  continue;
697
780
  const err = outcome.reason;
698
- hlog('unload failed', { err: err instanceof Error ? err.message : String(err) });
781
+ hlog('unload failed', { err: errorMessage(err) });
782
+ }
783
+ }
784
+ async _unloadIdleModels() {
785
+ if (!this.opened || this.closing || this.suspended)
786
+ return;
787
+ hlog('idle unload', { models: this._loadedModels().map((entry) => entry.model) });
788
+ const released = await Promise.allSettled([
789
+ evictIdle(this._models),
790
+ evictIdle(this._embedModels),
791
+ ...[...this._retiredEmbedModels].map((pool) => this._collectRetiredEmbedPool(pool)),
792
+ evictIdle(this._ttsModels),
793
+ evictIdle(this._transcriptionModels),
794
+ this._imageRuntime?.unloadIdle(),
795
+ this._musicRuntime?.unloadIdle(),
796
+ this._translationRuntime.unload()
797
+ ]);
798
+ for (const outcome of released) {
799
+ if (outcome.status !== 'rejected')
800
+ continue;
801
+ hlog('idle unload failed', { err: errorMessage(outcome.reason) });
699
802
  }
803
+ invalidateModelStatusCache(this._modelStatus);
700
804
  }
701
805
  async resume() {
702
806
  hlog('resume called');
703
- if (this._state === HarnessState.RESUME)
807
+ if (this._state === HarnessState.RESUME) {
704
808
  return;
705
- if (this.closing)
809
+ }
810
+ if (this.closing) {
706
811
  return;
812
+ }
707
813
  if (this._state === HarnessState.PRESUSPEND) {
708
814
  this._state = HarnessState.RESUME;
709
815
  return;
@@ -726,22 +832,26 @@ export class Harness extends ReadyResource {
726
832
  }
727
833
  async _close() {
728
834
  hlog('closing');
835
+ this._idle.close();
729
836
  this._overlayWatcher?.stop();
730
837
  this._overlayWatcher = null;
731
838
  const pending = this._sdk.peek();
732
- if (pending)
839
+ if (pending) {
733
840
  this._loads.drain(await pending);
841
+ }
734
842
  await Promise.all([this._releaseRuntimes(), closePythonWorker()]);
735
843
  const sdk = await this._sdk.peek()?.catch(noop);
736
- if (sdk)
844
+ if (sdk) {
737
845
  await sdk.close();
846
+ }
738
847
  }
739
848
  download(args) {
740
849
  const projection = (args.addon ?? 'llm') === 'llm' ? this._projectionFor(args.model) : undefined;
741
850
  const multiFile = diffusionDownloadMetadata(args.model, args.addon) ??
742
851
  audioGenDownloadMetadata(args.model, args.addon);
743
852
  return download(async () => {
744
- await this.ready();
853
+ if (!this.opened)
854
+ await this.ready();
745
855
  return this._sdk.load();
746
856
  }, {
747
857
  ...args,
@@ -750,12 +860,17 @@ export class Harness extends ReadyResource {
750
860
  assetSizes: multiFile?.assetSizes
751
861
  }, () => invalidateModelStatusCache(this._modelStatus));
752
862
  }
753
- async embed({ model, texts, signal }) {
754
- await this.ready();
863
+ embed(context) {
864
+ return this._busy(() => this._embed(context));
865
+ }
866
+ async _embed({ model, texts, signal }) {
867
+ if (!this.opened)
868
+ await this.ready();
755
869
  if (this.suspended)
756
870
  throw new Error('harness is suspended');
757
- if (texts.length === 0)
871
+ if (texts.length === 0) {
758
872
  return [];
873
+ }
759
874
  const pool = this._embedModels;
760
875
  const faults = this._embedFaults;
761
876
  const generation = faults.generation;
@@ -766,7 +881,7 @@ export class Harness extends ReadyResource {
766
881
  let releaseAfter = null;
767
882
  try {
768
883
  loaded = await raceAbort(handle.load(), signal);
769
- const sdk = (await this._sdk.loadEmbed());
884
+ const sdk = await this._sdk.loadEmbed();
770
885
  job = this._embedLane(sdk, { modelId: loaded.modelId, texts });
771
886
  const vectors = await raceAbort(job, signal);
772
887
  faults.reset();
@@ -789,15 +904,18 @@ export class Harness extends ReadyResource {
789
904
  throw err;
790
905
  }
791
906
  finally {
792
- if (releaseAfter)
907
+ if (releaseAfter) {
793
908
  this._releaseEmbedResourceAfter(releaseAfter, pool, key);
794
- else
909
+ }
910
+ else {
795
911
  await this._releaseEmbedResource(pool, key);
912
+ }
796
913
  }
797
914
  }
798
915
  async _defaultVoiceSrc(category) {
799
- if (category !== 'tts')
916
+ if (category !== 'tts') {
800
917
  return defaultVoiceModel(category);
918
+ }
801
919
  try {
802
920
  const { capabilityProfile } = await this.capability();
803
921
  return defaultTtsFor(capabilityProfile).asset.src;
@@ -806,8 +924,9 @@ export class Harness extends ReadyResource {
806
924
  return defaultVoiceModel('tts');
807
925
  }
808
926
  }
809
- async _voiceJob(pool, category, model, signal, run) {
810
- await this.ready();
927
+ async _voiceJob(pool, { category, model, signal }, run) {
928
+ if (!this.opened)
929
+ await this.ready();
811
930
  if (this.suspended)
812
931
  throw new Error('harness is suspended');
813
932
  const name = model || (await this._defaultVoiceSrc(category));
@@ -826,8 +945,9 @@ export class Harness extends ReadyResource {
826
945
  }
827
946
  catch (err) {
828
947
  if (err instanceof AbortedError) {
829
- if (sdk && modelId)
948
+ if (sdk && modelId) {
830
949
  void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
950
+ }
831
951
  releaseAfter = job;
832
952
  }
833
953
  throw err;
@@ -836,19 +956,25 @@ export class Harness extends ReadyResource {
836
956
  const release = () => {
837
957
  releaseModel(pool, name);
838
958
  };
839
- if (releaseAfter)
959
+ if (releaseAfter) {
840
960
  void releaseAfter.then(release, release);
841
- else
961
+ }
962
+ else {
842
963
  release();
964
+ }
843
965
  }
844
966
  }
845
- async transcribe({ model, audio, signal }) {
846
- return this._voiceJob(this._transcriptionModels, 'transcription', model, signal, async (sdk, id) => {
967
+ transcribe(context) {
968
+ return this._busy(() => this._transcribe(context));
969
+ }
970
+ async _transcribe({ model, audio, signal }) {
971
+ return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, async (sdk, id) => {
847
972
  const pcm = await pcmForTranscription(audio);
848
- if (signal?.aborted)
973
+ if (signal?.aborted) {
849
974
  throw new AbortedError();
975
+ }
850
976
  if (pcm && pcm.length >= TRANSCRIBE_STREAM_MIN_BYTES) {
851
- return transcribeStreaming(sdk, id, pcm, signal);
977
+ return transcribeStreaming(sdk, pcm, { modelId: id, signal });
852
978
  }
853
979
  return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
854
980
  });
@@ -856,12 +982,19 @@ export class Harness extends ReadyResource {
856
982
  detectLanguage(context) {
857
983
  return detectLanguage(context);
858
984
  }
859
- async speak({ model, text, signal }) {
860
- const samples = await this._voiceJob(this._ttsModels, 'tts', model, signal, (sdk, id) => speakOnce(sdk, id, text));
985
+ speak(context) {
986
+ return this._busy(() => this._speak(context));
987
+ }
988
+ async _speak({ model, text, signal }) {
989
+ const samples = await this._voiceJob(this._ttsModels, { category: 'tts', model, signal }, (sdk, id) => speakOnce(sdk, id, text));
861
990
  return { audio: packSamples(samples), sampleRate: SUPERTONIC_SAMPLE_RATE };
862
991
  }
863
- async *speakStream({ model, text, signal }) {
864
- await this.ready();
992
+ speakStream(context) {
993
+ return this._busyStream(() => this._speakStream(context));
994
+ }
995
+ async *_speakStream({ model, text, signal }) {
996
+ if (!this.opened)
997
+ await this.ready();
865
998
  if (this.suspended)
866
999
  throw new Error('harness is suspended');
867
1000
  const name = model || (await this._defaultVoiceSrc('tts'));
@@ -871,18 +1004,21 @@ export class Harness extends ReadyResource {
871
1004
  let sdk = null;
872
1005
  let modelId = null;
873
1006
  const cancel = () => {
874
- if (sdk && modelId)
875
- void sdk.cancel({ modelId, operation: 'inference' }).catch(() => { });
1007
+ if (sdk && modelId) {
1008
+ void sdk.cancel({ modelId, operation: 'inference' }).catch(noop);
1009
+ }
876
1010
  };
877
1011
  signal?.addEventListener('abort', cancel, { once: true });
878
1012
  const job = this._voiceLane(async () => {
879
1013
  try {
880
1014
  modelId = (await handle.load()).modelId;
881
1015
  sdk = (await this._sdk.loadVoice());
882
- if (signal?.aborted)
1016
+ if (signal?.aborted) {
883
1017
  return;
884
- for await (const chunk of speakStreamOnce(sdk, modelId, text))
1018
+ }
1019
+ for await (const chunk of speakStreamOnce(sdk, modelId, text)) {
885
1020
  chunks.push(chunk);
1021
+ }
886
1022
  }
887
1023
  finally {
888
1024
  chunks.end();
@@ -896,31 +1032,46 @@ export class Harness extends ReadyResource {
896
1032
  }
897
1033
  finally {
898
1034
  signal?.removeEventListener('abort', cancel);
899
- if (!settled)
1035
+ if (!settled) {
900
1036
  cancel();
1037
+ }
901
1038
  void job.catch(noop).finally(() => {
902
1039
  releaseModel(pool, name);
903
1040
  });
904
1041
  }
905
1042
  }
906
1043
  async list(options = {}) {
907
- await this.ready();
908
- const sdk = (await this._sdk.load());
909
- return listModels(sdk, options, this._modelStatus, (name) => this._projectionFor(name));
1044
+ if (!this.opened)
1045
+ await this.ready();
1046
+ const sdk = await this._sdk.load();
1047
+ const [rows, custom] = await Promise.all([
1048
+ listModels(sdk, options, {
1049
+ cache: this._modelStatus,
1050
+ projectionFor: (name) => this._projectionFor(name)
1051
+ }),
1052
+ customModelRows(options, {
1053
+ modelsDir: this._modelsDir,
1054
+ loaded: new Set(this._loadedModels().map((entry) => entry.model))
1055
+ })
1056
+ ]);
1057
+ return mergeCustomRows(rows, custom);
910
1058
  }
911
1059
  async deleteModel(name) {
912
- await this.ready();
913
- const sdk = (await this._sdk.load());
914
- const result = await deleteModelFiles(sdk, name);
1060
+ if (!this.opened)
1061
+ await this.ready();
1062
+ const sdk = await this._sdk.load();
1063
+ const result = await deleteModelFiles(sdk, name, { modelsDir: this._modelsDir });
915
1064
  invalidateModelStatusCache(this._modelStatus);
916
1065
  return summarizeDelete(result);
917
1066
  }
918
1067
  async clearKvCache() {
919
- await this.ready();
1068
+ if (!this.opened)
1069
+ await this.ready();
920
1070
  return deleteAllKvCaches(() => this._sdk.load());
921
1071
  }
922
1072
  async deleteKvCache(kvCacheKey) {
923
- await this.ready();
1073
+ if (!this.opened)
1074
+ await this.ready();
924
1075
  return deleteKvCacheKey(() => this._sdk.load(), kvCacheKey);
925
1076
  }
926
1077
  async infraModels(profile) {
@@ -964,8 +1115,12 @@ export class Harness extends ReadyResource {
964
1115
  });
965
1116
  return this._capabilityInflight;
966
1117
  }
967
- async warmModel({ model, agent, signal }) {
968
- await this.ready();
1118
+ warmModel(context) {
1119
+ return this._busy(() => this._warmModel(context));
1120
+ }
1121
+ async _warmModel({ model, agent, signal }) {
1122
+ if (!this.opened)
1123
+ await this.ready();
969
1124
  if (this.suspended)
970
1125
  return;
971
1126
  const projectionModel = this._projectionFor(model);
@@ -979,8 +1134,9 @@ export class Harness extends ReadyResource {
979
1134
  const pool = this._models;
980
1135
  hlog('warm model', { model, resident: isResident(pool, key) });
981
1136
  if (!isResident(pool, key)) {
982
- if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS)))
1137
+ if (!(await this._makeRoomForCompletion(signal, WARM_ROOM_WAIT_MS))) {
983
1138
  return;
1139
+ }
984
1140
  await this._imageRuntime?.unloadIdle().catch(noop);
985
1141
  }
986
1142
  try {
@@ -990,11 +1146,15 @@ export class Harness extends ReadyResource {
990
1146
  releaseModel(pool, key);
991
1147
  }
992
1148
  }
993
- async *invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
1149
+ invoke(context) {
1150
+ return this._busyStream(() => this._invoke(context));
1151
+ }
1152
+ async *_invoke({ capability, args, enhance, agent, attach, fetchAttachment, progress, signal }) {
994
1153
  if (!this.opened)
995
1154
  await this.ready();
996
- if (this.suspended)
1155
+ if (this.suspended) {
997
1156
  return yield { type: 'aborted' };
1157
+ }
998
1158
  hlog('invoke start', { capability, enhance });
999
1159
  try {
1000
1160
  yield* invoke({
@@ -1016,8 +1176,12 @@ export class Harness extends ReadyResource {
1016
1176
  hlog('invoke end', { capability });
1017
1177
  }
1018
1178
  }
1019
- async *completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1020
- await this.ready();
1179
+ completion(context) {
1180
+ return this._busyStream(() => this._completion(context));
1181
+ }
1182
+ async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1183
+ if (!this.opened)
1184
+ await this.ready();
1021
1185
  if (this.suspended)
1022
1186
  throw new Error('harness is suspended');
1023
1187
  const projectionModel = this._projectionFor(model);
@@ -1112,16 +1276,12 @@ export class Harness extends ReadyResource {
1112
1276
  yield { type: 'aborted' };
1113
1277
  }
1114
1278
  else {
1115
- const message = err instanceof Error ? err.message : String(err);
1279
+ const message = errorMessage(err);
1116
1280
  hlog('completion failed', { model, ...resolutionDetail(err), err: message });
1117
- if (isSdkModelWedgedError(err)) {
1118
- const wedged = await this._evictWedgedModel(model, key);
1119
- yield { type: 'error', message: wedged.message };
1120
- if (wedged.fatal)
1121
- process.exit(1);
1122
- }
1123
- else {
1124
- yield { type: 'error', message };
1281
+ const wedged = isSdkModelWedgedError(err) ? await this._evictWedgedModel(model, key) : null;
1282
+ yield { type: 'error', message: wedged ? wedged.message : message };
1283
+ if (wedged?.fatal) {
1284
+ process.exit(1);
1125
1285
  }
1126
1286
  }
1127
1287
  }
@@ -1135,14 +1295,19 @@ export class Harness extends ReadyResource {
1135
1295
  _residentEnhancer() {
1136
1296
  return async (prompt, signal) => {
1137
1297
  const lease = await leaseResidentModel(this._models, (model) => !isTranslationModel(model));
1138
- if (!lease)
1298
+ if (!lease) {
1139
1299
  return null;
1300
+ }
1140
1301
  try {
1141
1302
  const sdk = await this._sdk.load();
1142
- return await expandImagePrompt(sdk, lease.modelId, lease.model, prompt, signal);
1303
+ return await expandImagePrompt(sdk, prompt, {
1304
+ modelId: lease.modelId,
1305
+ model: lease.model,
1306
+ signal
1307
+ });
1143
1308
  }
1144
1309
  catch (err) {
1145
- hlog('enhance failed', { err: err instanceof Error ? err.message : String(err) });
1310
+ hlog('enhance failed', { err: errorMessage(err) });
1146
1311
  return null;
1147
1312
  }
1148
1313
  finally {
@@ -1151,7 +1316,11 @@ export class Harness extends ReadyResource {
1151
1316
  };
1152
1317
  }
1153
1318
  async _makeRoomForCompletion(signal, deadlineMs = MODEL_ROOM_WAIT_MS) {
1154
- const roomed = await awaitModelCapacity(this._models, this._completionModelCap, deadlineMs, 250, signal);
1319
+ const roomed = await awaitModelCapacity(this._models, {
1320
+ cap: this._completionModelCap,
1321
+ deadlineMs,
1322
+ signal
1323
+ });
1155
1324
  if (!roomed) {
1156
1325
  hlog('model room wait gave up', {
1157
1326
  cap: this._completionModelCap,
@@ -1164,10 +1333,13 @@ export class Harness extends ReadyResource {
1164
1333
  const faults = (this._modelFaults.get(model) ?? 0) + 1;
1165
1334
  this._modelFaults.set(model, faults);
1166
1335
  const leased = leaseCount(this._models, key) > 0;
1167
- const evicted = leased ? false : await evictModelWithin(this._models, key, WEDGED_EVICT_WAIT_MS);
1336
+ const evicted = leased
1337
+ ? false
1338
+ : await evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_WAIT_MS });
1168
1339
  hlog('wedged model evicted', { model, faults, evicted, leased });
1169
- if (leased)
1340
+ if (leased) {
1170
1341
  this._evictAfterLeases(model, key);
1342
+ }
1171
1343
  if (!evicted && !leased) {
1172
1344
  return {
1173
1345
  message: `${model} is stuck in device memory — restarting the model runtime`,
@@ -1182,9 +1354,27 @@ export class Harness extends ReadyResource {
1182
1354
  };
1183
1355
  }
1184
1356
  _evictAfterLeases(model, key) {
1185
- void evictModelWithin(this._models, key, WEDGED_EVICT_LEASED_MS)
1357
+ void evictModelWithin(this._models, key, { deadlineMs: WEDGED_EVICT_LEASED_MS })
1186
1358
  .then((evicted) => hlog('wedged model evicted after leases', { model, evicted }))
1187
1359
  .catch(noop);
1188
1360
  }
1361
+ async _busy(run) {
1362
+ await this._idle.enter();
1363
+ try {
1364
+ return await run();
1365
+ }
1366
+ finally {
1367
+ this._idle.exit();
1368
+ }
1369
+ }
1370
+ async *_busyStream(run) {
1371
+ await this._idle.enter();
1372
+ try {
1373
+ yield* run();
1374
+ }
1375
+ finally {
1376
+ this._idle.exit();
1377
+ }
1378
+ }
1189
1379
  }
1190
1380
  function noop() { }