@qvac/harness 0.8.5 → 0.8.7

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 (88) hide show
  1. package/CHANGELOG.md +1278 -472
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +42 -2
  4. package/dist-lib/index.js +447 -58
  5. package/dist-lib/lib/audio-transcode-bare.d.ts +1 -1
  6. package/dist-lib/lib/audio-transcode-bare.js +22 -25
  7. package/dist-lib/lib/audio-transcode-node.d.ts +1 -1
  8. package/dist-lib/lib/audio-transcode-node.js +1 -3
  9. package/dist-lib/lib/capabilities/audio-resample.d.ts +1 -1
  10. package/dist-lib/lib/capabilities/audio-resample.js +7 -8
  11. package/dist-lib/lib/capabilities/completion.d.ts +6 -3
  12. package/dist-lib/lib/capabilities/completion.js +96 -49
  13. package/dist-lib/lib/capabilities/download.d.ts +3 -1
  14. package/dist-lib/lib/capabilities/download.js +1 -1
  15. package/dist-lib/lib/capabilities/preview-model-source.d.ts +1 -0
  16. package/dist-lib/lib/capabilities/preview-model-source.js +1 -0
  17. package/dist-lib/lib/capabilities/tool-selection.d.ts +1 -3
  18. package/dist-lib/lib/capabilities/tool-selection.js +2 -4
  19. package/dist-lib/lib/capabilities/tool-turns.d.ts +0 -1
  20. package/dist-lib/lib/capabilities/tool-turns.js +0 -6
  21. package/dist-lib/lib/capabilities/usage-breakdown.js +4 -1
  22. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  23. package/dist-lib/lib/capabilities/voice.js +94 -26
  24. package/dist-lib/lib/connect.d.ts +4 -1
  25. package/dist-lib/lib/connect.js +16 -1
  26. package/dist-lib/lib/contract-types.d.ts +12 -0
  27. package/dist-lib/lib/helpers/tool-call-markup.d.ts +1 -0
  28. package/dist-lib/lib/helpers/tool-call-markup.js +3 -0
  29. package/dist-lib/lib/models/context-choice.d.ts +15 -0
  30. package/dist-lib/lib/models/context-choice.js +55 -0
  31. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  32. package/dist-lib/lib/models/download-gate.js +73 -0
  33. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  34. package/dist-lib/lib/models/memory-cap.js +21 -0
  35. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  36. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  37. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  38. package/dist-lib/lib/models/model-catalog.js +35 -3
  39. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  40. package/dist-lib/lib/models/model-handle.js +20 -2
  41. package/dist-lib/lib/models/model-list.d.ts +8 -1
  42. package/dist-lib/lib/models/model-list.js +23 -0
  43. package/dist-lib/lib/models/model-pool.d.ts +35 -5
  44. package/dist-lib/lib/models/model-pool.js +97 -32
  45. package/dist-lib/lib/models/sdk-faults.d.ts +1 -0
  46. package/dist-lib/lib/models/sdk-faults.js +6 -0
  47. package/dist-lib/lib/office/engine/pptx/picture-area.d.ts +23 -0
  48. package/dist-lib/lib/office/engine/pptx/picture-area.js +84 -0
  49. package/dist-lib/lib/office/engine/pptx/presentation.d.ts +2 -0
  50. package/dist-lib/lib/office/engine/pptx/presentation.js +55 -17
  51. package/dist-lib/lib/office/engine/pptx/types.d.ts +1 -0
  52. package/dist-lib/lib/office/engine/xlsx/workbook.d.ts +1 -0
  53. package/dist-lib/lib/office/engine/xlsx/workbook.js +34 -3
  54. package/dist-lib/lib/serve.js +23 -3
  55. package/dist-lib/lib/spawn.js +4 -10
  56. package/dist-lib/lib/tools/exec.d.ts +4 -1
  57. package/dist-lib/lib/tools/exec.js +55 -11
  58. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  59. package/dist-lib/lib/tools/generate-image.js +5 -2
  60. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  61. package/dist-lib/lib/tools/generate-music.js +5 -2
  62. package/dist-lib/lib/tools/office/ops.js +1 -1
  63. package/dist-lib/lib/tools/office.js +29 -6
  64. package/dist-lib/lib/tools/skills/catalog.d.ts +1 -0
  65. package/dist-lib/lib/tools/skills/catalog.js +2 -0
  66. package/dist-lib/lib/tools/skills/cli-schema-check.d.ts +1 -0
  67. package/dist-lib/lib/tools/skills/cli-schema-check.js +3 -0
  68. package/dist-lib/lib/tools/skills/cli-schema-load.js +10 -0
  69. package/dist-lib/lib/tools/skills/cli-schema-types.d.ts +2 -0
  70. package/dist-lib/lib/tools/skills/cli-schema.js +6 -3
  71. package/dist-lib/lib/tools/skills/frontmatter-parser.d.ts +1 -0
  72. package/dist-lib/lib/tools/skills/manifest-parse.d.ts +1 -0
  73. package/dist-lib/lib/tools/skills/manifest-parse.js +12 -0
  74. package/dist-lib/lib/tools/skills/mention.d.ts +0 -1
  75. package/dist-lib/lib/tools/skills/mention.js +0 -3
  76. package/dist-lib/lib/tools/translate.d.ts +3 -1
  77. package/dist-lib/lib/tools/translate.js +6 -1
  78. package/dist-lib/schema/harness.js +43 -4
  79. package/dist-lib/spec/bind.js +9 -0
  80. package/dist-lib/spec/calls.js +3 -0
  81. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  82. package/dist-lib/spec/hrpc/index.js +39 -3
  83. package/dist-lib/spec/hrpc/messages.js +101 -13
  84. package/dist-lib/spec/hyperschema/index.js +101 -13
  85. package/dist-lib/spec/hyperschema/schema.json +62 -1
  86. package/dist-lib/spec/hyperschema/types.d.ts +20 -2
  87. package/package.json +8 -10
  88. package/worker/harness-sidecar.mjs +2 -1
package/dist-lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'fs';
2
+ import ScopeLock from 'scope-lock';
2
3
  import { platform, totalmem } from 'os';
3
4
  import process from 'process';
4
5
  import ReadyResource from 'ready-resource';
@@ -17,15 +18,18 @@ import { download } from './lib/capabilities/download.js';
17
18
  import { createEmbedLane } from './lib/capabilities/embed.js';
18
19
  import { deleteAllKvCaches, deleteKvCacheKey } from './lib/capabilities/kv-cache.js';
19
20
  import { pcmForTranscription } from './lib/capabilities/audio-resample.js';
20
- import { buildWavBuffer } from './lib/wav-encode.js';
21
- import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
21
+ import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeAudio, SUPERTONIC_SAMPLE_RATE } from './lib/capabilities/voice.js';
22
22
  import { pushQueue } from './lib/queue.js';
23
23
  import { AbortedError, raceAbort } from './lib/helpers/race-abort.js';
24
- import { audioGenDownloadMetadata, categoryModelConfig, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, curatedModelAsset, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
25
- import { createModelStatusCache, customModelRows, invalidateModelStatusCache, mergeCustomRows, listModels } from './lib/models/model-list.js';
24
+ import { audioGenDownloadMetadata, categoryModelConfig, customProjectionMap, defaultProjectionMap, defaultTtsFor, defaultVoiceModel, diffusionDownloadMetadata, assetBytes, curatedModelAsset, customContextLength, customModelSrc, infraModelsFor, modelAddonOf, modelNameToCapabilityProfile, musicModelSet, catalogModelName, residentSrc, setCustomModels } from './lib/models/model-catalog.js';
25
+ import { createModelStatusCache, customModelRows, invalidateModelStatusCache, isModelSourceCached, mergeCustomRows, listModels } from './lib/models/model-list.js';
26
+ import { createDownloadGate } from './lib/models/download-gate.js';
26
27
  import { deleteModelFiles, summarizeDelete } from './lib/models/model-delete.js';
27
- import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
28
+ import { createEmbedPool, createLoadTracker, createModelPool, createVoicePool, awaitModelCapacity, evictIdle, evictIdleWhere, evictIdleWithin, evictModel, evictModelWithin, isResident, leaseResidentModel, leaseCount, loadedModels, loadingModels, settledEntries, settledModels, modelKey, modelOf, releaseModel, resolveLoadOptions, progressState } from './lib/models/model-pool.js';
29
+ import { memoryCapBytes, nextVictim, VictimRank } from './lib/models/memory-cap.js';
28
30
  import { defaultContextSize } from './lib/models/context.js';
31
+ import { ContextChoice } from './lib/models/context-choice.js';
32
+ import { parseGgufHeader, readGgufHeaderBytes } from './lib/models/gguf-header.js';
29
33
  import { resolveMainGpu } from './lib/models/gpu-selection.js';
30
34
  import { createIdleReaper } from './lib/models/idle-unload.js';
31
35
  import { openclCacheDir } from './lib/models/opencl-cache.js';
@@ -41,7 +45,7 @@ import { ensureBundledSkillsDir } from './lib/tools/skills/bundled-dir.js';
41
45
  import { ensureSkillsOverlayDir } from './lib/tools/skills/overlay-dir.js';
42
46
  import { copyBaseForEdit, deleteOverlaySkill, listManagedSkills, readSkillFile, writeSkillFile } from './lib/tools/skills/overlay-crud.js';
43
47
  import { watchSkillsOverlay } from './lib/tools/skills/overlay-watch.js';
44
- import { errorMessage } from './lib/guards.js';
48
+ import { errorMessage, isRecord } from './lib/guards.js';
45
49
  import { hlog } from './lib/log.js';
46
50
  import { harness } from './spec/index.js';
47
51
  import { installResolutionDetector, resolutionDetail } from './lib/resolution-detector.js';
@@ -75,7 +79,6 @@ export { harnessHooks } from './lib/hooks/registry.js';
75
79
  export { EgressGuardHook, SkillCatalogGuardHook } from './lib/hooks/built-in/index.js';
76
80
  const HarnessState = harness.getEnum('@harness/state');
77
81
  const BIN_VERSION_PROBE_TTL_MS = 45_000;
78
- const TRANSCRIBE_STREAM_MIN_BYTES = 16_000 * 2 * 60;
79
82
  const TRANSCRIPTION_MODEL_LABEL = 'transcription model';
80
83
  const DIFFUSION_EVICT_WAIT_MS = 15_000;
81
84
  const MUSIC_EVICT_WAIT_MS = 15_000;
@@ -91,6 +94,23 @@ const TTS_MODEL_CONFIG = {
91
94
  ttsSpeed: 1.175,
92
95
  ttsNumInferenceSteps: 5
93
96
  };
97
+ function fitSrc(modelSrc) {
98
+ if (typeof modelSrc === 'string')
99
+ return modelSrc;
100
+ const { src, name, sha256Checksum } = modelSrc;
101
+ return { src, name, sha256Checksum };
102
+ }
103
+ function downloadBytes(modelSrc) {
104
+ if (typeof modelSrc === 'string') {
105
+ return assetBytes(modelSrc);
106
+ }
107
+ return isRecord(modelSrc) && typeof modelSrc.expectedSize === 'number'
108
+ ? modelSrc.expectedSize
109
+ : undefined;
110
+ }
111
+ function stagesBytes(srcs) {
112
+ return srcs.reduce((sum, src) => sum + (assetBytes(src) ?? 0), 0);
113
+ }
94
114
  export class Harness extends ReadyResource {
95
115
  _tools;
96
116
  _sdk;
@@ -98,6 +118,8 @@ export class Harness extends ReadyResource {
98
118
  _pinned = new Set();
99
119
  _replying = [];
100
120
  _modelStatus = createModelStatusCache();
121
+ _downloads;
122
+ _hold;
101
123
  _models;
102
124
  _embedModels;
103
125
  _retiredEmbedModels;
@@ -140,6 +162,12 @@ export class Harness extends ReadyResource {
140
162
  _capabilityInflight = null;
141
163
  _modelFaults = new Map();
142
164
  _idle;
165
+ _sizeLoads = false;
166
+ _capBytes = null;
167
+ _reserved = new Map();
168
+ _contexts = new ContextChoice(() => this._contextProfile());
169
+ _admission = new ScopeLock();
170
+ _byHand = new Map();
143
171
  constructor({ tools = [], sdkLoader, skillsDir, modelsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, completionModelCap, credentialStore, platform, gpuStats } = {}) {
144
172
  super();
145
173
  this._sdk = sdkLoader ?? createSdkLoader();
@@ -164,11 +192,19 @@ export class Harness extends ReadyResource {
164
192
  device: embedConfig.device,
165
193
  gpuLayers: embedConfig.gpuLayers
166
194
  });
195
+ this._downloads = createDownloadGate({
196
+ isCached: (model, addon) => this._isModelCached(model, addon),
197
+ onHeld: ({ model, addon }) => this._notifyModel({ status: 'held', model, addon }),
198
+ onDropped: ({ model, addon }) => this._notifyModel({ status: 'error', model, addon, message: 'aborted' })
199
+ });
200
+ const hold = (model, addon, signal) => this._downloads.hold(catalogModelName(model, addon), addon, signal);
201
+ this._hold = hold;
167
202
  const imageTransfers = new Map();
168
203
  const image = typeof imageModel === 'string' ? { model: imageModel } : imageModel;
169
204
  this._imageRuntime = image
170
205
  ? createDiffusionRuntime({
171
206
  ...image,
207
+ hold,
172
208
  loads: this._loads,
173
209
  loadSdk: () => this._loadDiffusionSdk(),
174
210
  onProgress: (model, progress) => {
@@ -191,9 +227,13 @@ export class Harness extends ReadyResource {
191
227
  invalidateModelStatusCache(this._modelStatus);
192
228
  this.emit('model', { ...transfer, status: 'downloaded' });
193
229
  },
194
- onBeforeLoad: async (signal) => {
230
+ onBeforeLoad: async (signal, srcs) => {
195
231
  if (this._models.destroyed)
196
232
  return;
233
+ if (this._capBytes !== null) {
234
+ await this._makeRoomForGenerator(srcs, signal);
235
+ return;
236
+ }
197
237
  const evicted = await awaitModelCapacity(this._models, {
198
238
  cap: 1,
199
239
  deadlineMs: DIFFUSION_EVICT_WAIT_MS,
@@ -212,12 +252,17 @@ export class Harness extends ReadyResource {
212
252
  this._musicRuntime = music
213
253
  ? createMusicRuntime({
214
254
  ...music,
255
+ hold,
215
256
  loads: this._loads,
216
257
  loadSdk: () => this._loadMusicSdk(),
217
258
  onProgress: (model, progress) => this.emit('model', progressState(model, progress)),
218
- onBeforeLoad: async (signal) => {
259
+ onBeforeLoad: async (signal, srcs) => {
219
260
  if (this._models.destroyed)
220
261
  return;
262
+ if (this._capBytes !== null) {
263
+ await this._makeRoomForGenerator(srcs, signal);
264
+ return;
265
+ }
221
266
  const evicted = await awaitModelCapacity(this._models, {
222
267
  cap: 1,
223
268
  deadlineMs: MUSIC_EVICT_WAIT_MS,
@@ -233,6 +278,7 @@ export class Harness extends ReadyResource {
233
278
  })
234
279
  : null;
235
280
  this._translationRuntime = createTranslationRuntime({
281
+ hold,
236
282
  loads: this._loads,
237
283
  loadSdk: () => this._loadTranslationSdk(),
238
284
  onProgress: (model, progress) => this.emit('model', progressState(model, progress))
@@ -523,13 +569,45 @@ export class Harness extends ReadyResource {
523
569
  setModelIdleTimeout(ms) {
524
570
  this._idle.configure(ms);
525
571
  }
572
+ async setModelMemoryCap(percent) {
573
+ this._sizeLoads = true;
574
+ const { mobile, memoryBudget } = await this.capability();
575
+ this._capBytes =
576
+ percent > 0 && !mobile && memoryBudget
577
+ ? memoryCapBytes(percent, memoryBudget.availableBytes)
578
+ : null;
579
+ hlog('memory cap', { percent, capBytes: this._capBytes });
580
+ if (this._capBytes !== null) {
581
+ await this._busy(() => this._admit(() => this._makeRoom(0, { generators: true })));
582
+ }
583
+ }
584
+ async memoryUsage() {
585
+ return { usedBytes: this._settledBytes() };
586
+ }
587
+ setDownloadsAllowed({ allowed }) {
588
+ this._downloads.setAllowed(allowed);
589
+ }
590
+ async _isModelCached(model, addon) {
591
+ const row = (await this.list({ type: addon })).find((entry) => entry.name === model);
592
+ if (row) {
593
+ return row.isCached;
594
+ }
595
+ const sdk = await this._sdk.load();
596
+ return isModelSourceCached(sdk, { model, addon, modelsDir: this._modelsDir });
597
+ }
526
598
  previewModelSource(context) {
527
599
  return previewModelSource(context, { assessFit: (query) => this._assessFit(query) });
528
600
  }
529
- async _assessFit({ sha256Checksum, name, contextTokens }) {
601
+ async _assessFit({ src, sha256Checksum, name, contextTokens }) {
530
602
  const sdk = await this._sdk.load();
531
603
  const result = await sdk.assessModelFit({
532
- models: [{ model: { sha256Checksum, name }, workload: { kind: 'llm', contextTokens } }]
604
+ models: [
605
+ {
606
+ modelSrc: { src, sha256Checksum, name },
607
+ modelType: 'llm',
608
+ modelConfig: { ctx_size: contextTokens }
609
+ }
610
+ ]
533
611
  });
534
612
  const model = result.models[0];
535
613
  return {
@@ -607,7 +685,7 @@ export class Harness extends ReadyResource {
607
685
  const key = modelKey(model, loadOpts);
608
686
  const pool = this._models;
609
687
  if (!isResident(pool, key)) {
610
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
688
+ await this._makeRoomForCompletion(model, { ctxSize: this._ctxSizeOf(key, loadOpts), signal });
611
689
  }
612
690
  try {
613
691
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
@@ -644,7 +722,10 @@ export class Harness extends ReadyResource {
644
722
  const loadSdk = this._sdk.load;
645
723
  return createModelPool(loadSdk, (state) => this._notifyModel(state), {
646
724
  deviceConfig: this._deviceConfig(),
647
- loads: this._loads
725
+ loads: this._loads,
726
+ beforeLoad: (load) => this._beforeLoad(load),
727
+ chooseContext: (load, tier) => this._chooseContext(load, tier),
728
+ hold: this._hold
648
729
  });
649
730
  }
650
731
  _createEmbedPool() {
@@ -653,7 +734,9 @@ export class Harness extends ReadyResource {
653
734
  faults: this._embedFaults,
654
735
  batchSize: this._embedBatchSize,
655
736
  openclCacheDir,
656
- loads: this._loads
737
+ loads: this._loads,
738
+ beforeLoad: (load) => this._beforeLoad(load),
739
+ hold: this._hold
657
740
  });
658
741
  }
659
742
  _createTtsPool() {
@@ -663,7 +746,9 @@ export class Harness extends ReadyResource {
663
746
  addon: 'tts',
664
747
  modelConfig: TTS_MODEL_CONFIG,
665
748
  modelConfigFor: categoryModelConfig,
666
- loads: this._loads
749
+ loads: this._loads,
750
+ beforeLoad: (load) => this._beforeLoad(load),
751
+ hold: this._hold
667
752
  });
668
753
  }
669
754
  _createTranscriptionPool() {
@@ -673,7 +758,9 @@ export class Harness extends ReadyResource {
673
758
  addon: 'parakeet',
674
759
  modelConfig: { useGPU: this._transcriptionDevice !== 'cpu' },
675
760
  openclCacheDir,
676
- loads: this._loads
761
+ loads: this._loads,
762
+ beforeLoad: (load) => this._beforeLoad(load),
763
+ hold: this._hold
677
764
  });
678
765
  }
679
766
  _notifyModel(state) {
@@ -703,6 +790,10 @@ export class Harness extends ReadyResource {
703
790
  releaseModel(pool, key);
704
791
  await this._collectRetiredEmbedPool(pool);
705
792
  }
793
+ _collectRetiredEmbedPoolAfter(settled, pool) {
794
+ const collect = () => this._collectRetiredEmbedPool(pool);
795
+ settled.then(collect, collect).catch(noop);
796
+ }
706
797
  _releaseEmbedResourceAfter(settled, pool, key) {
707
798
  settled
708
799
  .then(() => this._releaseEmbedResource(pool, key), () => this._releaseEmbedResource(pool, key))
@@ -758,11 +849,14 @@ export class Harness extends ReadyResource {
758
849
  }
759
850
  this._state = HarnessState.SUSPEND;
760
851
  hlog('suspending', { models: loadedModels(this._models).map((entry) => entry.model) });
852
+ this._downloads.abandon();
761
853
  const pending = this._sdk.peek();
762
854
  if (pending) {
763
855
  this._loads.drain(await pending);
764
856
  }
765
857
  await this._releaseRuntimes();
858
+ this._reserved.clear();
859
+ this._byHand.clear();
766
860
  await this._sdkLifecycle('suspend');
767
861
  invalidateModelStatusCache(this._modelStatus);
768
862
  hlog('suspended');
@@ -856,6 +950,7 @@ export class Harness extends ReadyResource {
856
950
  async _close() {
857
951
  hlog('closing');
858
952
  this._idle.close();
953
+ this._downloads.abandon();
859
954
  this._overlayWatcher?.stop();
860
955
  this._overlayWatcher = null;
861
956
  const pending = this._sdk.peek();
@@ -881,7 +976,12 @@ export class Harness extends ReadyResource {
881
976
  projection,
882
977
  companions: multiFile?.companions,
883
978
  assetSizes: multiFile?.assetSizes
884
- }, () => invalidateModelStatusCache(this._modelStatus));
979
+ }, (event) => {
980
+ invalidateModelStatusCache(this._modelStatus);
981
+ if (event.type === 'complete') {
982
+ this._downloads.downloaded();
983
+ }
984
+ });
885
985
  }
886
986
  embed(context) {
887
987
  return this._busy(() => this._embed(context));
@@ -902,6 +1002,7 @@ export class Harness extends ReadyResource {
902
1002
  let loaded = null;
903
1003
  let job = null;
904
1004
  let releaseAfter = null;
1005
+ let collectAfter = null;
905
1006
  try {
906
1007
  loaded = await raceAbort(handle.load(), signal);
907
1008
  const sdk = await this._sdk.loadEmbed();
@@ -912,7 +1013,8 @@ export class Harness extends ReadyResource {
912
1013
  }
913
1014
  catch (err) {
914
1015
  if (err instanceof AbortedError) {
915
- releaseAfter = job ?? (this._retiredEmbedModels.has(pool) ? handle.peek() : null);
1016
+ releaseAfter = job;
1017
+ collectAfter = job === null && this._retiredEmbedModels.has(pool) ? handle.peek() : null;
916
1018
  }
917
1019
  if (isSdkGpuDeviceLostError(err)) {
918
1020
  faults.recordFault(generation);
@@ -930,6 +1032,10 @@ export class Harness extends ReadyResource {
930
1032
  if (releaseAfter) {
931
1033
  this._releaseEmbedResourceAfter(releaseAfter, pool, key);
932
1034
  }
1035
+ else if (collectAfter) {
1036
+ releaseModel(pool, key);
1037
+ this._collectRetiredEmbedPoolAfter(collectAfter, pool);
1038
+ }
933
1039
  else {
934
1040
  await this._releaseEmbedResource(pool, key);
935
1041
  }
@@ -992,16 +1098,7 @@ export class Harness extends ReadyResource {
992
1098
  return this._busy(() => this._transcribe(context));
993
1099
  }
994
1100
  async _transcribe({ model, audio, signal }) {
995
- return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, async (sdk, id) => {
996
- const pcm = await pcmForTranscription(audio);
997
- if (signal?.aborted) {
998
- throw new AbortedError();
999
- }
1000
- if (pcm && pcm.length >= TRANSCRIBE_STREAM_MIN_BYTES) {
1001
- return transcribeStreaming(sdk, pcm, { modelId: id, signal });
1002
- }
1003
- return transcribeOnce(sdk, id, pcm ? buildWavBuffer(pcm) : audio);
1004
- });
1101
+ return this._voiceJob(this._transcriptionModels, { category: 'transcription', model, signal }, (sdk, id) => transcribeAudio(sdk, pcmForTranscription(audio), { modelId: id, signal, audio }));
1005
1102
  }
1006
1103
  detectLanguage(context) {
1007
1104
  return detectLanguage(context);
@@ -1033,21 +1130,25 @@ export class Harness extends ReadyResource {
1033
1130
  }
1034
1131
  };
1035
1132
  signal?.addEventListener('abort', cancel, { once: true });
1036
- const job = this._voiceLane(async () => {
1133
+ const job = (async () => {
1037
1134
  try {
1038
- modelId = (await handle.load()).modelId;
1039
- sdk = (await this._sdk.loadVoice());
1040
- if (signal?.aborted) {
1041
- return;
1042
- }
1043
- for await (const chunk of speakStreamOnce(sdk, modelId, text)) {
1044
- chunks.push(chunk);
1045
- }
1135
+ const loadedId = (await raceAbort(handle.load(), signal)).modelId;
1136
+ modelId = loadedId;
1137
+ const voiceSdk = (await this._sdk.loadVoice());
1138
+ sdk = voiceSdk;
1139
+ await this._voiceLane(async () => {
1140
+ if (signal?.aborted) {
1141
+ return;
1142
+ }
1143
+ for await (const chunk of speakStreamOnce(voiceSdk, loadedId, text)) {
1144
+ chunks.push(chunk);
1145
+ }
1146
+ });
1046
1147
  }
1047
1148
  finally {
1048
1149
  chunks.end();
1049
1150
  }
1050
- });
1151
+ })();
1051
1152
  let settled = false;
1052
1153
  try {
1053
1154
  yield* chunks;
@@ -1075,9 +1176,11 @@ export class Harness extends ReadyResource {
1075
1176
  const loaded = this._settledModels();
1076
1177
  const loading = this._loadingModels();
1077
1178
  const custom = await customModelRows(options, { modelsDir: this._modelsDir, loaded });
1179
+ const memory = this._memoryByModel();
1078
1180
  return mergeCustomRows(rows, custom).map((row) => ({
1079
1181
  ...row,
1080
- isLoading: loading.has(row.name)
1182
+ isLoading: loading.has(row.name),
1183
+ memoryBytes: memory.get(row.name) ?? memory.get(residentSrc(row.name, row.type))
1081
1184
  }));
1082
1185
  }
1083
1186
  async deleteModel(name) {
@@ -1159,7 +1262,7 @@ export class Harness extends ReadyResource {
1159
1262
  if (!roomed) {
1160
1263
  return;
1161
1264
  }
1162
- await this._unloadIdleGenerators();
1265
+ await this._yieldGenerators();
1163
1266
  }
1164
1267
  await this._loadAndRelease(target, signal);
1165
1268
  }
@@ -1175,20 +1278,20 @@ export class Harness extends ReadyResource {
1175
1278
  const target = this._residency(model, agent);
1176
1279
  const resident = isResident(target.pool, target.key);
1177
1280
  hlog('load model', { model, resident });
1281
+ if (!resident && !(await this._admitByHand(model, target))) {
1282
+ return { loaded: false, reason: 'memory' };
1283
+ }
1178
1284
  if (target.pool !== this._models) {
1179
- await this._loadAndRelease(target, signal);
1285
+ await this._loadByHand(target, signal);
1180
1286
  return { loaded: true };
1181
1287
  }
1182
1288
  if (!resident) {
1183
- if (!(await this._fits(model, target.ctxSize))) {
1184
- return { loaded: false, reason: 'memory' };
1185
- }
1186
- await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
1187
- await this._unloadIdleGenerators();
1289
+ await evictIdleWhere(this._models, this._replacedBy(model));
1290
+ await this._yieldGenerators();
1188
1291
  }
1189
1292
  this._pinned.add(target.key);
1190
1293
  try {
1191
- await this._loadAndRelease(target, signal);
1294
+ await this._loadByHand(target, signal);
1192
1295
  }
1193
1296
  catch (err) {
1194
1297
  this._pinned.delete(target.key);
@@ -1228,11 +1331,15 @@ export class Harness extends ReadyResource {
1228
1331
  await this._musicRuntime?.unloadIdle().catch(noop);
1229
1332
  }
1230
1333
  async _fits(model, ctxSize) {
1334
+ return (await this._curatedFit(model, ctxSize))?.fit !== 'likely-too-large';
1335
+ }
1336
+ async _curatedFit(model, ctxSize) {
1231
1337
  const asset = curatedModelAsset(model, 'llm');
1232
1338
  if (!asset) {
1233
- return true;
1339
+ return null;
1234
1340
  }
1235
- const assessed = await this._assessFit({
1341
+ return this._assessFit({
1342
+ src: asset.src,
1236
1343
  sha256Checksum: asset.sha256Checksum,
1237
1344
  name: asset.name,
1238
1345
  contextTokens: ctxSize || defaultContextSize(asset)
@@ -1240,7 +1347,273 @@ export class Harness extends ReadyResource {
1240
1347
  hlog('fit estimate failed', { model, err: errorMessage(err) });
1241
1348
  return null;
1242
1349
  });
1243
- return assessed?.fit !== 'likely-too-large';
1350
+ }
1351
+ async _admitByHand(model, target) {
1352
+ if (this._capBytes === null) {
1353
+ return (target.pool !== this._models ||
1354
+ this._alone(model, target) ||
1355
+ this._fits(model, target.ctxSize));
1356
+ }
1357
+ return this._admit(async () => {
1358
+ if (isResident(target.pool, target.key) || this._byHand.has(target.key)) {
1359
+ return true;
1360
+ }
1361
+ const sized = await this._sizeByHand(model, target);
1362
+ const capBytes = this._capBytes;
1363
+ const replaced = this._replacedEntries(model, target);
1364
+ const used = this._usedBytes() - replaced.reduce((sum, { bytes }) => sum + (bytes ?? 0), 0);
1365
+ const alone = this._alone(model, target);
1366
+ const fits = capBytes === null || alone || sized.bytes === undefined || used + sized.bytes <= capBytes;
1367
+ if ((sized.tooLarge && !alone) || !fits) {
1368
+ return false;
1369
+ }
1370
+ this._byHand.set(target.key, sized.ctxSize);
1371
+ if (sized.bytes !== undefined) {
1372
+ this._reserved.set(target.key, sized.bytes);
1373
+ }
1374
+ return true;
1375
+ });
1376
+ }
1377
+ _replacedBy(model) {
1378
+ return (key) => modelOf(key) === model && !this._replying.includes(key);
1379
+ }
1380
+ _replacedEntries(model, target) {
1381
+ const replacedBy = this._replacedBy(model);
1382
+ return target.pool === this._models
1383
+ ? settledEntries(this._models).filter(({ key, refs }) => refs === 0 && replacedBy(key))
1384
+ : [];
1385
+ }
1386
+ _alone(model, target) {
1387
+ const settled = this._pools().flatMap((pool) => settledEntries(pool));
1388
+ const loading = this._pools().some((pool) => loadingModels(pool).length > 0);
1389
+ return (!loading &&
1390
+ this._reserved.size === 0 &&
1391
+ settled.length === this._replacedEntries(model, target).length);
1392
+ }
1393
+ async _sizeByHand(model, target) {
1394
+ const addon = modelAddonOf(model);
1395
+ const modelSrc = curatedModelAsset(model, addon) ?? customModelSrc(model, addon) ?? model;
1396
+ const ctxSize = target.pool === this._models
1397
+ ? target.ctxSize || defaultContextSize(modelSrc, customContextLength(model))
1398
+ : undefined;
1399
+ const sized = await this._sizeLoad({
1400
+ key: target.key,
1401
+ model,
1402
+ modelSrc,
1403
+ modelType: target.modelType,
1404
+ modelConfig: ctxSize === undefined
1405
+ ? undefined
1406
+ : { ctx_size: ctxSize, projectionModelSrc: this._projectionFor(model) }
1407
+ });
1408
+ return { ...sized, ctxSize };
1409
+ }
1410
+ async _loadByHand(target, signal) {
1411
+ const loading = target.open().load();
1412
+ void loading.then(noop, noop).then(() => {
1413
+ if (this._byHand.delete(target.key)) {
1414
+ this._reserved.delete(target.key);
1415
+ }
1416
+ });
1417
+ try {
1418
+ await raceAbort(loading, signal);
1419
+ }
1420
+ finally {
1421
+ releaseModel(target.pool, target.key);
1422
+ }
1423
+ }
1424
+ async _admit(run) {
1425
+ await this._admission.lock();
1426
+ try {
1427
+ return await run();
1428
+ }
1429
+ finally {
1430
+ this._admission.unlock();
1431
+ }
1432
+ }
1433
+ async _makeRoomForGenerator(srcs, signal) {
1434
+ if (signal?.aborted) {
1435
+ throw new AbortedError();
1436
+ }
1437
+ await this._makeRoom(stagesBytes(srcs), { replying: true });
1438
+ if (signal?.aborted) {
1439
+ throw new AbortedError();
1440
+ }
1441
+ }
1442
+ async _yieldGenerators() {
1443
+ if (this._capBytes === null) {
1444
+ await this._unloadIdleGenerators();
1445
+ }
1446
+ }
1447
+ async _beforeLoad(load) {
1448
+ if (!this._sizeLoads)
1449
+ return undefined;
1450
+ const bytes = this._byHand.has(load.key)
1451
+ ? await this._admit(() => this._reserveByHand(load))
1452
+ : await this._admit(() => this._reserve(load));
1453
+ return { bytes, release: () => this._reserved.delete(load.key) };
1454
+ }
1455
+ async _reserveByHand(load) {
1456
+ const admittedAt = this._byHand.get(load.key);
1457
+ this._byHand.delete(load.key);
1458
+ if (load.modelConfig?.['ctx_size'] !== admittedAt) {
1459
+ const { bytes } = await this._sizeLoad(load);
1460
+ if (bytes !== undefined) {
1461
+ this._reserved.set(load.key, bytes);
1462
+ }
1463
+ }
1464
+ return this._reserved.get(load.key);
1465
+ }
1466
+ async _reserve(load) {
1467
+ const sized = await this._sizeLoad(load);
1468
+ if (this._capBytes !== null) {
1469
+ await this._makeRoom(sized.bytes ?? 0, {
1470
+ generators: true,
1471
+ tooLarge: sized.tooLarge,
1472
+ recheck: async () => (await this._sizeLoad(load)).tooLarge
1473
+ });
1474
+ }
1475
+ if (sized.bytes !== undefined) {
1476
+ this._reserved.set(load.key, sized.bytes);
1477
+ }
1478
+ return sized.bytes;
1479
+ }
1480
+ async _sizeLoad({ model, modelSrc, modelType, modelConfig }) {
1481
+ const downloaded = downloadBytes(modelSrc);
1482
+ const { projectionModelSrc, ...config } = modelConfig ?? {};
1483
+ const projection = downloadBytes(projectionModelSrc) ?? 0;
1484
+ try {
1485
+ const sdk = await this._sdk.load();
1486
+ const { models } = await sdk.assessModelFit({
1487
+ models: [{ modelSrc: fitSrc(modelSrc), modelType, modelConfig: config }]
1488
+ });
1489
+ const [fit] = models;
1490
+ const bytes = fit?.estimate?.upperBoundBytes ?? fit?.floorBytes ?? downloaded;
1491
+ return {
1492
+ bytes: bytes === undefined ? undefined : bytes + projection,
1493
+ tooLarge: fit?.verdict === 'likely-too-large',
1494
+ confirmed: fit?.verdict === 'likely-fits' &&
1495
+ (fit.evidence === 'calibration' || fit.evidence === 'native-fit')
1496
+ };
1497
+ }
1498
+ catch (err) {
1499
+ hlog('fit estimate failed', { model, err: errorMessage(err) });
1500
+ return {
1501
+ bytes: downloaded === undefined ? undefined : downloaded + projection,
1502
+ tooLarge: false,
1503
+ confirmed: false
1504
+ };
1505
+ }
1506
+ }
1507
+ async _chooseContext(load, tier) {
1508
+ if (isTranslationModel(load.model)) {
1509
+ return tier;
1510
+ }
1511
+ const ctxSize = await this._contexts.choose(load.key, tier, {
1512
+ trained: () => this._trainedContext(load),
1513
+ fits: async (rung) => {
1514
+ const sized = await this._sizeLoad({
1515
+ ...load,
1516
+ modelConfig: { ...load.modelConfig, ctx_size: rung }
1517
+ });
1518
+ const room = this._capRoom(load.key);
1519
+ return sized.confirmed && (room === null || (sized.bytes ?? Infinity) <= room);
1520
+ }
1521
+ });
1522
+ hlog('context chosen', { model: load.model, tier, ctxSize });
1523
+ return ctxSize;
1524
+ }
1525
+ _capRoom(key) {
1526
+ const cap = this._capBytes;
1527
+ return cap === null ? null : cap - (this._usedBytes() - (this._reserved.get(key) ?? 0));
1528
+ }
1529
+ async _contextProfile() {
1530
+ const { capabilityProfile, memoryBudget } = await this.capability();
1531
+ return memoryBudget ? capabilityProfile : undefined;
1532
+ }
1533
+ async _trainedContext({ model, modelSrc }) {
1534
+ const custom = customContextLength(model);
1535
+ if (custom || typeof modelSrc === 'string') {
1536
+ return custom;
1537
+ }
1538
+ const sdk = await this._sdk.load();
1539
+ const info = await sdk.getModelInfo({ name: modelSrc.name }).catch(() => null);
1540
+ const file = info?.cacheFiles[0]?.path;
1541
+ const bytes = file ? await readGgufHeaderBytes(file) : null;
1542
+ return (bytes && parseGgufHeader(bytes)?.contextLength) || undefined;
1543
+ }
1544
+ _ctxSizeOf(key, loadOpts) {
1545
+ return loadOpts.ctxSize || this._contexts.chosen(key);
1546
+ }
1547
+ async _makeRoom(need, room) {
1548
+ const capBytes = this._capBytes;
1549
+ if (capBytes === null) {
1550
+ return;
1551
+ }
1552
+ const tried = new Set();
1553
+ let tooLarge = room.tooLarge ?? false;
1554
+ while (this._usedBytes() + need > capBytes || tooLarge) {
1555
+ const victim = nextVictim(this._capVictims(room).filter(({ id }) => !tried.has(id)));
1556
+ if (!victim) {
1557
+ hlog('model over memory cap', { need, used: this._usedBytes(), capBytes, tooLarge });
1558
+ return;
1559
+ }
1560
+ tried.add(victim.id);
1561
+ await victim.evict();
1562
+ if (tooLarge && room.recheck) {
1563
+ tooLarge = await room.recheck();
1564
+ }
1565
+ }
1566
+ }
1567
+ _capVictims({ replying = false, generators = false }) {
1568
+ const victims = [];
1569
+ if (generators) {
1570
+ victims.push({
1571
+ id: 'generators',
1572
+ rank: VictimRank.generator,
1573
+ lastUsed: 0,
1574
+ evict: () => this._unloadIdleGenerators()
1575
+ });
1576
+ }
1577
+ for (const [index, pool] of this._pools().entries()) {
1578
+ for (const { key, refs, lastUsed } of settledEntries(pool)) {
1579
+ const inReply = this._replying.includes(key);
1580
+ if (refs > 0 || (inReply && !replying)) {
1581
+ continue;
1582
+ }
1583
+ victims.push({
1584
+ id: `${index}:${key}`,
1585
+ rank: this._victimRank(pool, key, inReply),
1586
+ lastUsed,
1587
+ evict: () => evictIdleWithin(pool, key, { deadlineMs: WEDGED_EVICT_WAIT_MS })
1588
+ });
1589
+ }
1590
+ }
1591
+ return victims;
1592
+ }
1593
+ _victimRank(pool, key, inReply) {
1594
+ if (pool !== this._models)
1595
+ return VictimRank.companion;
1596
+ if (inReply)
1597
+ return VictimRank.replying;
1598
+ return this._pinned.has(key) ? VictimRank.pinned : VictimRank.requested;
1599
+ }
1600
+ _usedBytes() {
1601
+ return (this._settledBytes() + [...this._reserved.values()].reduce((sum, bytes) => sum + bytes, 0));
1602
+ }
1603
+ _settledBytes() {
1604
+ return this._pools()
1605
+ .flatMap((pool) => settledEntries(pool))
1606
+ .reduce((sum, { bytes }) => sum + (bytes ?? 0), 0);
1607
+ }
1608
+ _memoryByModel() {
1609
+ const memory = new Map();
1610
+ for (const { key, bytes } of this._pools().flatMap((pool) => settledEntries(pool))) {
1611
+ if (bytes === undefined)
1612
+ continue;
1613
+ const model = modelOf(key);
1614
+ memory.set(model, (memory.get(model) ?? 0) + bytes);
1615
+ }
1616
+ return memory;
1244
1617
  }
1245
1618
  async _yieldPins(model, ctxSize) {
1246
1619
  for (const key of this._pinned) {
@@ -1261,18 +1634,25 @@ export class Harness extends ReadyResource {
1261
1634
  return {
1262
1635
  pool: this._models,
1263
1636
  key,
1264
- ctxSize: loadOpts.ctxSize,
1637
+ modelType: 'llamacpp-completion',
1638
+ ctxSize: this._ctxSizeOf(key, loadOpts),
1265
1639
  open: () => this._models.open(key, model, loadOpts)
1266
1640
  };
1267
1641
  }
1268
1642
  if (addon === 'embeddings') {
1269
1643
  const key = this._embedFaults.key(residentSrc(model, addon));
1270
- return { pool: this._embedModels, key, open: () => this._embedModels.open(key, model) };
1644
+ return {
1645
+ pool: this._embedModels,
1646
+ key,
1647
+ modelType: 'llamacpp-embedding',
1648
+ open: () => this._embedModels.open(key, model)
1649
+ };
1271
1650
  }
1272
1651
  if (addon === 'tts' || addon === 'parakeet') {
1273
1652
  const pool = addon === 'tts' ? this._ttsModels : this._transcriptionModels;
1274
1653
  const key = residentSrc(model, addon);
1275
- return { pool, key, open: () => pool.open(key, key) };
1654
+ const modelType = addon === 'tts' ? 'tts-ggml' : 'parakeet-transcription';
1655
+ return { pool, key, modelType, open: () => pool.open(key, key) };
1276
1656
  }
1277
1657
  throw new Error(`${model} loads for each generation and is not kept in memory`);
1278
1658
  }
@@ -1317,7 +1697,7 @@ export class Harness extends ReadyResource {
1317
1697
  completion(context) {
1318
1698
  return this._busyStream(() => this._completion(context));
1319
1699
  }
1320
- async *_completion({ model, agent, messages, cacheKey, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1700
+ async *_completion({ model, agent, messages, typedText, cacheKey, session, thinkingDefault, toolConfigs, generationParams, responseFormat, captureInferenceCalls, grantedDirs, grantedDrives, driveOp, grantedWriteDirs, approve, attach, fetchAttachment, ragSearch, progress, askUser, signal }) {
1321
1701
  if (!this.opened)
1322
1702
  await this.ready();
1323
1703
  if (this.suspended)
@@ -1329,13 +1709,16 @@ export class Harness extends ReadyResource {
1329
1709
  const resident = isResident(pool, key);
1330
1710
  hlog('completion start', { model, resident, projection: Boolean(projectionModel) });
1331
1711
  if (!resident) {
1332
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1333
- await this._unloadIdleGenerators();
1712
+ await this._makeRoomForCompletion(model, { ctxSize: this._ctxSizeOf(key, loadOpts), signal });
1713
+ await this._yieldGenerators();
1334
1714
  }
1335
1715
  const acquireRound = async () => {
1336
1716
  if (!isResident(pool, key)) {
1337
- await this._makeRoomForCompletion(model, { ctxSize: loadOpts.ctxSize, signal });
1338
- await this._unloadIdleGenerators();
1717
+ await this._makeRoomForCompletion(model, {
1718
+ ctxSize: this._ctxSizeOf(key, loadOpts),
1719
+ signal
1720
+ });
1721
+ await this._yieldGenerators();
1339
1722
  }
1340
1723
  try {
1341
1724
  const { modelId } = await raceAbort(pool.open(key, model, loadOpts).load(), signal);
@@ -1382,11 +1765,13 @@ export class Harness extends ReadyResource {
1382
1765
  agent,
1383
1766
  ctxSize,
1384
1767
  cacheKey,
1768
+ session,
1385
1769
  thinkingDefault,
1386
1770
  generationParams,
1387
1771
  responseFormat,
1388
1772
  captureInferenceCalls,
1389
1773
  messages: turns,
1774
+ typedText,
1390
1775
  tools: this._tools,
1391
1776
  skillCatalog: await this._completionSkills(agent?.id ?? null, model),
1392
1777
  toolConfigs,
@@ -1449,6 +1834,10 @@ export class Harness extends ReadyResource {
1449
1834
  };
1450
1835
  }
1451
1836
  async _makeRoomForCompletion(model, { ctxSize, signal, deadlineMs = MODEL_ROOM_WAIT_MS } = {}) {
1837
+ if (this._capBytes !== null) {
1838
+ await evictIdleWhere(this._models, (key) => modelOf(key) === model && !this._replying.includes(key));
1839
+ return true;
1840
+ }
1452
1841
  const roomed = await awaitModelCapacity(this._models, {
1453
1842
  cap: this._completionModelCap,
1454
1843
  deadlineMs,