@qvac/harness 0.8.5 → 0.8.6

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 (53) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +49 -0
  3. package/dist-lib/index.d.ts +31 -2
  4. package/dist-lib/index.js +365 -53
  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 +5 -2
  12. package/dist-lib/lib/capabilities/completion.js +22 -4
  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/usage-breakdown.js +4 -1
  18. package/dist-lib/lib/capabilities/voice.d.ts +5 -1
  19. package/dist-lib/lib/capabilities/voice.js +94 -26
  20. package/dist-lib/lib/connect.d.ts +4 -1
  21. package/dist-lib/lib/connect.js +14 -1
  22. package/dist-lib/lib/contract-types.d.ts +10 -0
  23. package/dist-lib/lib/models/download-gate.d.ts +20 -0
  24. package/dist-lib/lib/models/download-gate.js +73 -0
  25. package/dist-lib/lib/models/memory-cap.d.ts +13 -0
  26. package/dist-lib/lib/models/memory-cap.js +21 -0
  27. package/dist-lib/lib/models/model-catalog-constants.d.ts +3 -1
  28. package/dist-lib/lib/models/model-catalog-constants.js +9 -1
  29. package/dist-lib/lib/models/model-catalog.d.ts +6 -2
  30. package/dist-lib/lib/models/model-catalog.js +35 -3
  31. package/dist-lib/lib/models/model-handle.d.ts +8 -1
  32. package/dist-lib/lib/models/model-handle.js +20 -2
  33. package/dist-lib/lib/models/model-list.d.ts +8 -1
  34. package/dist-lib/lib/models/model-list.js +23 -0
  35. package/dist-lib/lib/models/model-pool.d.ts +33 -5
  36. package/dist-lib/lib/models/model-pool.js +97 -29
  37. package/dist-lib/lib/serve.js +18 -2
  38. package/dist-lib/lib/tools/generate-image.d.ts +4 -2
  39. package/dist-lib/lib/tools/generate-image.js +5 -2
  40. package/dist-lib/lib/tools/generate-music.d.ts +4 -2
  41. package/dist-lib/lib/tools/generate-music.js +5 -2
  42. package/dist-lib/lib/tools/translate.d.ts +3 -1
  43. package/dist-lib/lib/tools/translate.js +6 -1
  44. package/dist-lib/schema/harness.js +41 -3
  45. package/dist-lib/spec/bind.js +9 -0
  46. package/dist-lib/spec/calls.js +3 -0
  47. package/dist-lib/spec/hrpc/hrpc.json +39 -0
  48. package/dist-lib/spec/hrpc/index.js +39 -3
  49. package/dist-lib/spec/hrpc/messages.js +93 -10
  50. package/dist-lib/spec/hyperschema/index.js +93 -10
  51. package/dist-lib/spec/hyperschema/schema.json +57 -1
  52. package/dist-lib/spec/hyperschema/types.d.ts +19 -2
  53. package/package.json +4 -4
@@ -1,2 +1,2 @@
1
+ export declare function decodeToPcm16kMono(input: Uint8Array): AsyncGenerator<Uint8Array>;
1
2
  export declare function transcodeToWav16kMono(input: Uint8Array): Promise<Uint8Array | null>;
2
- export declare function transcodeToPcm16kMono(input: Uint8Array): Promise<Uint8Array | null>;
@@ -92,13 +92,12 @@ const YIELD_EVERY_PACKETS = 32;
92
92
  function yieldToLoop() {
93
93
  return new Promise((resolve) => setTimeout(resolve, 0));
94
94
  }
95
- async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
96
- const chunks = [];
95
+ async function* decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
97
96
  const packet = new ffmpeg.Packet();
98
97
  const flush = new ffmpeg.Packet();
99
98
  const frame = new ffmpeg.Frame();
100
99
  const samples = new ffmpeg.Samples();
101
- const target = { frame, samples, chunks, rate };
100
+ const target = { frame, samples, chunks: [], rate };
102
101
  let resampler = null;
103
102
  let packets = 0;
104
103
  try {
@@ -107,13 +106,15 @@ async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
107
106
  resampler = drainDecodedFrames(decoder, resampler, target);
108
107
  }
109
108
  packet.unref();
109
+ yield* target.chunks.splice(0);
110
110
  if (++packets % YIELD_EVERY_PACKETS === 0) {
111
111
  await yieldToLoop();
112
112
  }
113
113
  }
114
114
  decoder.sendPacket(flush);
115
115
  resampler = drainDecodedFrames(decoder, resampler, target);
116
- chunks.push(...flushResampler(resampler, samples, rate));
116
+ yield* target.chunks.splice(0);
117
+ yield* flushResampler(resampler, samples, rate);
117
118
  }
118
119
  finally {
119
120
  resampler?.destroy();
@@ -121,35 +122,31 @@ async function decodeToPcmChunks(inputFormat, { stream, decoder, rate }) {
121
122
  flush.destroy();
122
123
  packet.destroy();
123
124
  }
124
- return chunks;
125
125
  }
126
- async function decodeInputToPcm(input, rate) {
127
- let inputFormat = null;
126
+ export async function* decodeToPcm16kMono(input) {
127
+ const inputFormat = new ffmpeg.InputFormatContext(bufferReadIO(input));
128
128
  let decoder = null;
129
129
  try {
130
- const format = new ffmpeg.InputFormatContext(bufferReadIO(input));
131
- inputFormat = format;
132
- const stream = format.getBestStream(ffmpeg.constants.mediaTypes.AUDIO);
130
+ const stream = inputFormat.getBestStream(ffmpeg.constants.mediaTypes.AUDIO);
133
131
  decoder = stream.decoder();
134
132
  decoder.open();
135
- const chunks = await decodeToPcmChunks(format, { stream, decoder, rate });
136
- return chunks.length === 0 ? null : b4a.concat(chunks);
137
- }
138
- catch (e) {
139
- hlog('audio decode/resample failed', { error: errorMessage(e) });
140
- return null;
133
+ yield* decodeToPcmChunks(inputFormat, { stream, decoder, rate: TARGET_SAMPLE_RATE });
141
134
  }
142
135
  finally {
143
- if (decoder) {
144
- decoder.destroy();
145
- }
146
- inputFormat?.destroy();
136
+ decoder?.destroy();
137
+ inputFormat.destroy();
147
138
  }
148
139
  }
149
140
  export async function transcodeToWav16kMono(input) {
150
- const pcm = await decodeInputToPcm(input, TARGET_SAMPLE_RATE);
151
- return pcm ? buildWavBuffer(pcm) : null;
152
- }
153
- export async function transcodeToPcm16kMono(input) {
154
- return decodeInputToPcm(input, TARGET_SAMPLE_RATE);
141
+ const chunks = [];
142
+ try {
143
+ for await (const chunk of decodeToPcm16kMono(input)) {
144
+ chunks.push(chunk);
145
+ }
146
+ }
147
+ catch (e) {
148
+ hlog('audio decode/resample failed', { error: errorMessage(e) });
149
+ return null;
150
+ }
151
+ return chunks.length === 0 ? null : buildWavBuffer(b4a.concat(chunks));
155
152
  }
@@ -1,2 +1,2 @@
1
1
  export declare function transcodeToWav16kMono(_input: Uint8Array): Promise<Uint8Array | null>;
2
- export declare function transcodeToPcm16kMono(_input: Uint8Array): Promise<Uint8Array | null>;
2
+ export declare function decodeToPcm16kMono(_input: Uint8Array): AsyncGenerator<Uint8Array>;
@@ -1,6 +1,4 @@
1
1
  export async function transcodeToWav16kMono(_input) {
2
2
  return null;
3
3
  }
4
- export async function transcodeToPcm16kMono(_input) {
5
- return null;
6
- }
4
+ export async function* decodeToPcm16kMono(_input) { }
@@ -11,5 +11,5 @@ export interface ResampledAudio {
11
11
  readonly transcoded: boolean;
12
12
  }
13
13
  export declare function resampleForTranscription(bytes: Uint8Array): Promise<ResampledAudio>;
14
- export declare function pcmForTranscription(bytes: Uint8Array): Promise<Uint8Array | null>;
14
+ export declare function pcmForTranscription(bytes: Uint8Array): AsyncGenerator<Uint8Array>;
15
15
  export {};
@@ -1,5 +1,5 @@
1
1
  import b4a from 'b4a';
2
- import { transcodeToPcm16kMono, transcodeToWav16kMono } from '#audio-transcode';
2
+ import { decodeToPcm16kMono, transcodeToWav16kMono } from '#audio-transcode';
3
3
  const TARGET_SAMPLE_RATE = 16000;
4
4
  const TARGET_CHANNELS = 1;
5
5
  const TARGET_BITS_PER_SAMPLE = 16;
@@ -73,12 +73,11 @@ function wavPcmData(bytes) {
73
73
  }
74
74
  return null;
75
75
  }
76
- export async function pcmForTranscription(bytes) {
77
- if (wavFormat(bytes) && !needsResample(bytes)) {
78
- const pcm = wavPcmData(bytes);
79
- if (pcm) {
80
- return pcm;
81
- }
76
+ export async function* pcmForTranscription(bytes) {
77
+ const pcm = wavFormat(bytes) && !needsResample(bytes) ? wavPcmData(bytes) : null;
78
+ if (pcm) {
79
+ yield pcm;
80
+ return;
82
81
  }
83
- return transcodeToPcm16kMono(bytes);
82
+ yield* decodeToPcm16kMono(bytes);
84
83
  }
@@ -1,4 +1,4 @@
1
- import type { CompletionFinal, Tool as SdkTool, ToolDialect } from '@qvac/sdk';
1
+ import type { CompletionEvent as SdkCompletionEvent, CompletionFinal, Tool as SdkTool, ToolDialect } from '@qvac/sdk';
2
2
  import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
3
3
  import { type TextEvent } from '../helpers/thinking.ts';
4
4
  import type { ToolCallDisplay } from '../tool-call-display.ts';
@@ -266,7 +266,7 @@ export interface InferenceTokens {
266
266
  export type CompletionEvent = TextEvent | ToolCallEvent | ToolResultEvent | ToolActivityEvent | ErrorEvent | AbortedEvent | MetricsEvent | InferenceCallEvent | InferenceSentEvent;
267
267
  export interface CompletionSdkResult {
268
268
  requestId?: string;
269
- tokenStream: AsyncIterable<string>;
269
+ events: AsyncIterable<SdkCompletionEvent>;
270
270
  final: Promise<CompletionFinal>;
271
271
  }
272
272
  export type ResponseFormat = {
@@ -296,6 +296,9 @@ export interface CompletionSdk {
296
296
  cancel?(params: {
297
297
  requestId: string;
298
298
  }): Promise<void>;
299
+ deleteCache?(params: {
300
+ kvCacheKey: string;
301
+ }): Promise<unknown>;
299
302
  }
300
303
  export interface RoundModelLease {
301
304
  modelId: string;
@@ -14,7 +14,7 @@ import { HookPipeline } from '../hooks/pipeline.js';
14
14
  import { fallbackToolLabel } from '../helpers/tool-label.js';
15
15
  import { hlog } from '../log.js';
16
16
  import { resolveLlmGenerationParams } from '../models/llm-presets.js';
17
- import { customToolDialect } from '../models/model-catalog.js';
17
+ import { toolDialectFor } from '../models/model-catalog.js';
18
18
  import { isContextOverflowError, isSdkModelWedgedError } from '../models/sdk-faults.js';
19
19
  import { annotateLastUserTime, skillsPromptSection, systemPromptParts } from '../prompt.js';
20
20
  import { isMetadataMethod } from '../tools/mcp/protocol.js';
@@ -459,6 +459,13 @@ function unprovenReplay(state, final) {
459
459
  }
460
460
  return state.unproven;
461
461
  }
462
+ async function* contentDeltas(events) {
463
+ for await (const event of events) {
464
+ if (event.type === 'contentDelta') {
465
+ yield event.text;
466
+ }
467
+ }
468
+ }
462
469
  async function* runRoundTokens(result, state, { sdk, signal, startInThink = false }) {
463
470
  const cancelThisRound = () => {
464
471
  if (result.requestId) {
@@ -466,7 +473,7 @@ async function* runRoundTokens(result, state, { sdk, signal, startInThink = fals
466
473
  }
467
474
  };
468
475
  signal?.addEventListener('abort', cancelThisRound, { once: true });
469
- const tokens = stopAtControlToken(result.tokenStream, (token) => {
476
+ const tokens = stopAtControlToken(contentDeltas(result.events), (token) => {
470
477
  state.stopToken = token;
471
478
  });
472
479
  let tokenEvents = 0;
@@ -674,6 +681,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
674
681
  let runError;
675
682
  const runOutcomes = createRunOutcomeTracker();
676
683
  const landed = runOutcomes.confirmedWrites;
684
+ let preloadCacheKey;
677
685
  async function* runTurn() {
678
686
  if (signal?.aborted)
679
687
  throw new AbortedError();
@@ -753,7 +761,7 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
753
761
  }
754
762
  skillIcon = skillEmoji(skillName) ?? skillIcon;
755
763
  }
756
- const toolDialect = model ? customToolDialect(model) : undefined;
764
+ const toolDialect = model ? toolDialectFor(model) : undefined;
757
765
  let turns = messages;
758
766
  let preloadedSkill = null;
759
767
  const preload = mention ?? resolveImplicitSkill(lastUserContent(messages), activeSkills);
@@ -834,7 +842,8 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
834
842
  }
835
843
  logSelection();
836
844
  let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...turns] : turns);
837
- let roundCacheKey = preloadedSkill === null ? cacheKey : undefined;
845
+ preloadCacheKey = childCacheKey(cacheKey, preloadedSkill, messages.length);
846
+ let roundCacheKey = preloadedSkill === null ? cacheKey : preloadCacheKey;
838
847
  const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
839
848
  const denied = new Set();
840
849
  const failed = new Map();
@@ -1323,7 +1332,16 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
1323
1332
  outcome: runOutcome,
1324
1333
  error: runError
1325
1334
  });
1335
+ if (preloadCacheKey) {
1336
+ await sdk.deleteCache?.({ kvCacheKey: preloadCacheKey }).catch(noop);
1337
+ }
1338
+ }
1339
+ }
1340
+ function childCacheKey(cacheKey, preloadedSkill, turnCount) {
1341
+ if (preloadedSkill === null || cacheKey === undefined) {
1342
+ return undefined;
1326
1343
  }
1344
+ return `${cacheKey}/p${turnCount}`;
1327
1345
  }
1328
1346
  function toolDecision({ auto, skipAll, approved }) {
1329
1347
  if (auto)
@@ -45,4 +45,6 @@ export type SdkOrLoader = DownloadSdk | (() => Promise<DownloadSdk>);
45
45
  export interface DownloadEventStream extends Readable {
46
46
  [Symbol.asyncIterator](): AsyncIterableIterator<DownloadEvent>;
47
47
  }
48
- export declare function download(sdkOrLoader: SdkOrLoader, args: DownloadArgs, onSettled?: () => void): DownloadEventStream;
48
+ type TerminalEvent = Exclude<DownloadEvent, DownloadProgressEvent>;
49
+ export declare function download(sdkOrLoader: SdkOrLoader, args: DownloadArgs, onSettled?: (event: TerminalEvent) => void): DownloadEventStream;
50
+ export {};
@@ -17,7 +17,7 @@ export function download(sdkOrLoader, args, onSettled) {
17
17
  settled = true;
18
18
  stream.push(event);
19
19
  stream.push(null);
20
- onSettled?.();
20
+ onSettled?.(event);
21
21
  };
22
22
  const cancelOp = () => {
23
23
  if (cancelSent || opDone || !requestId || !sdkRef?.cancel)
@@ -41,6 +41,7 @@ export type PreviewFetch = (url: string, init?: {
41
41
  redirect?: 'follow';
42
42
  }) => Promise<PreviewResponse>;
43
43
  export interface FitQuery {
44
+ src: string;
44
45
  sha256Checksum: string;
45
46
  name: string;
46
47
  contextTokens: number;
@@ -365,6 +365,7 @@ async function assess({ file, header }, taskType, deps) {
365
365
  }
366
366
  try {
367
367
  return await deps.assessFit({
368
+ src: file.src,
368
369
  sha256Checksum: file.sha256,
369
370
  name: header.name ?? file.label,
370
371
  contextTokens: defaultContextSize(file.src, header.contextLength)
@@ -1,8 +1,11 @@
1
+ import { toolDialectFor } from '../models/model-catalog.js';
1
2
  import { detectToolDialect } from '../models/tool-dialect.js';
2
3
  const QWEN35 = { prose: 4.38, schema: 4.08, toolBlockTokens: 189 };
3
4
  const DENSITY_BY_DIALECT = new Map([['qwen35', QWEN35]]);
4
5
  export function densityFor(modelName) {
5
- const dialect = modelName ? detectToolDialect(modelName, modelName) : undefined;
6
+ const dialect = modelName
7
+ ? (toolDialectFor(modelName) ?? detectToolDialect(modelName, modelName))
8
+ : undefined;
6
9
  return (dialect && DENSITY_BY_DIALECT.get(dialect)) ?? QWEN35;
7
10
  }
8
11
  export function promptUsageParts({ system, toolSchemas }) {
@@ -75,7 +75,11 @@ export interface TranscribeStreamingOptions {
75
75
  modelId: string;
76
76
  signal?: BareAbortSignal | undefined;
77
77
  }
78
- export declare function transcribeStreaming(sdk: VoiceSdk, pcm: Uint8Array, { modelId, signal }: TranscribeStreamingOptions): Promise<string>;
78
+ export interface TranscribeAudioOptions extends TranscribeStreamingOptions {
79
+ audio: Buffer;
80
+ }
81
+ export declare function transcribeAudio(sdk: VoiceSdk, pcm: AsyncIterable<Uint8Array>, { modelId, signal, audio }: TranscribeAudioOptions): Promise<string>;
82
+ export declare function transcribeStreaming(sdk: VoiceSdk, pcm: AsyncIterable<Uint8Array>, opts: TranscribeStreamingOptions): Promise<string>;
79
83
  export declare function speakStreamOnce(sdk: VoiceSdk, modelId: string, text: AsyncIterable<string>): AsyncGenerator<SpeakResult>;
80
84
  export declare function speakable(text: string): string;
81
85
  export declare function speakOnce(sdk: VoiceSdk, modelId: string, text: string): Promise<number[]>;
@@ -2,6 +2,7 @@ import b4a from 'b4a';
2
2
  import { errorMessage } from '../guards.js';
3
3
  import { AbortedError } from '../helpers/race-abort.js';
4
4
  import { hlog } from '../log.js';
5
+ import { buildWavBuffer } from '../wav-encode.js';
5
6
  import { normalizeForSpeech } from './speech-text.js';
6
7
  export const SUPERTONIC_SAMPLE_RATE = 44100;
7
8
  export function createVoiceLane() {
@@ -14,31 +15,19 @@ export function createVoiceLane() {
14
15
  }
15
16
  const TRANSCRIBE_SAMPLE_RATE = 16000;
16
17
  const TRANSCRIBE_CHUNK_BYTES = TRANSCRIBE_SAMPLE_RATE * 2;
18
+ const TRANSCRIBE_STREAM_MIN_BYTES = TRANSCRIBE_CHUNK_BYTES * 60;
19
+ const TRANSCRIBE_SEGMENT_SECONDS = 300;
17
20
  const PARAKEET_STREAMING = { emitPartials: false, chunkMs: 5000 };
18
21
  const TRANSCRIBE_WITH_METADATA = false;
19
22
  const TRANSCRIBE_PACE_MS = 40;
20
23
  function pace(ms) {
21
24
  return new Promise((resolve) => setTimeout(resolve, ms));
22
25
  }
23
- const TRANSCRIBE_DRAIN_IDLE_MS = 3000;
24
- const TRANSCRIBE_DRAIN_MAX_MS = 60_000;
25
- async function writePaced(session, data, signal) {
26
- for (let offset = 0; offset < data.length; offset += TRANSCRIBE_CHUNK_BYTES) {
26
+ async function writePaced(session, pcm, signal) {
27
+ for await (const second of pcm) {
27
28
  if (signal?.aborted)
28
29
  return;
29
- session.write(data.subarray(offset, offset + TRANSCRIBE_CHUNK_BYTES));
30
- await pace(TRANSCRIBE_PACE_MS);
31
- }
32
- }
33
- async function drainUntilIdle(lastPieceAt, streamClosed, signal) {
34
- const deadline = Date.now() + TRANSCRIBE_DRAIN_MAX_MS;
35
- for (;;) {
36
- if (signal?.aborted || streamClosed() || Date.now() >= deadline)
37
- return;
38
- const last = lastPieceAt();
39
- if (last > 0 && Date.now() - last >= TRANSCRIBE_DRAIN_IDLE_MS) {
40
- return;
41
- }
30
+ session.write(second);
42
31
  await pace(TRANSCRIBE_PACE_MS);
43
32
  }
44
33
  }
@@ -59,7 +48,93 @@ function transcribePiece(piece) {
59
48
  }
60
49
  return null;
61
50
  }
62
- export async function transcribeStreaming(sdk, pcm, { modelId, signal }) {
51
+ async function* inSeconds(pcm) {
52
+ let pending = [];
53
+ let size = 0;
54
+ for await (const chunk of pcm) {
55
+ pending.push(chunk);
56
+ size += chunk.length;
57
+ if (size < TRANSCRIBE_CHUNK_BYTES) {
58
+ continue;
59
+ }
60
+ const joined = pending.length === 1 ? chunk : b4a.concat(pending);
61
+ let offset = 0;
62
+ for (; joined.length - offset >= TRANSCRIBE_CHUNK_BYTES; offset += TRANSCRIBE_CHUNK_BYTES) {
63
+ yield joined.subarray(offset, offset + TRANSCRIBE_CHUNK_BYTES);
64
+ }
65
+ size = joined.length - offset;
66
+ pending = size > 0 ? [joined.subarray(offset)] : [];
67
+ }
68
+ if (size > 0) {
69
+ yield b4a.concat(pending);
70
+ }
71
+ }
72
+ async function readHead(seconds, signal) {
73
+ const head = { seconds: [], bytes: 0 };
74
+ try {
75
+ while (head.bytes < TRANSCRIBE_STREAM_MIN_BYTES && !signal?.aborted) {
76
+ const next = await seconds.next();
77
+ if (next.done) {
78
+ break;
79
+ }
80
+ head.seconds.push(next.value);
81
+ head.bytes += next.value.length;
82
+ }
83
+ }
84
+ catch (err) {
85
+ hlog('audio decode/resample failed', { error: errorMessage(err) });
86
+ return null;
87
+ }
88
+ return head;
89
+ }
90
+ async function* resume(head, rest) {
91
+ yield* head.seconds;
92
+ yield* { [Symbol.asyncIterator]: () => rest };
93
+ }
94
+ export async function transcribeAudio(sdk, pcm, { modelId, signal, audio }) {
95
+ const seconds = inSeconds(pcm);
96
+ const head = await readHead(seconds, signal);
97
+ if (signal?.aborted) {
98
+ await seconds.return(undefined);
99
+ throw new AbortedError();
100
+ }
101
+ if (!head || head.bytes === 0) {
102
+ return transcribeOnce(sdk, modelId, audio);
103
+ }
104
+ if (head.bytes < TRANSCRIBE_STREAM_MIN_BYTES) {
105
+ return transcribeOnce(sdk, modelId, buildWavBuffer(b4a.concat(head.seconds)));
106
+ }
107
+ try {
108
+ return await transcribeStreaming(sdk, resume(head, seconds), { modelId, signal });
109
+ }
110
+ finally {
111
+ await seconds.return(undefined);
112
+ }
113
+ }
114
+ async function* segment(first, rest) {
115
+ yield first;
116
+ for (let n = 1; n < TRANSCRIBE_SEGMENT_SECONDS; n++) {
117
+ const next = await rest.next();
118
+ if (next.done) {
119
+ return;
120
+ }
121
+ yield next.value;
122
+ }
123
+ }
124
+ export async function transcribeStreaming(sdk, pcm, opts) {
125
+ const seconds = pcm[Symbol.asyncIterator]();
126
+ const parts = [];
127
+ try {
128
+ for (let first = await seconds.next(); !first.done; first = await seconds.next()) {
129
+ parts.push(await transcribeSession(sdk, segment(first.value, seconds), opts));
130
+ }
131
+ }
132
+ finally {
133
+ await seconds.return?.();
134
+ }
135
+ return parts.filter(Boolean).join(' ');
136
+ }
137
+ async function transcribeSession(sdk, pcm, { modelId, signal }) {
63
138
  const session = await sdk.transcribeStream({
64
139
  modelId,
65
140
  metadata: TRANSCRIBE_WITH_METADATA,
@@ -67,27 +142,20 @@ export async function transcribeStreaming(sdk, pcm, { modelId, signal }) {
67
142
  });
68
143
  const onAbort = () => session.destroy();
69
144
  signal?.addEventListener('abort', onAbort, { once: true });
70
- let lastPieceAt = 0;
71
- let streamClosed = false;
72
145
  const collect = (async () => {
73
146
  let out = '';
74
147
  for await (const piece of session) {
75
- lastPieceAt = Date.now();
76
148
  const seg = transcribePiece(piece);
77
149
  if (!seg) {
78
150
  continue;
79
151
  }
80
152
  out += !out || seg.append ? seg.text : ` ${seg.text}`;
81
153
  }
82
- streamClosed = true;
83
154
  return out.replace(/\s+/g, ' ').trim();
84
155
  })();
85
- collect.catch(() => {
86
- streamClosed = true;
87
- });
156
+ collect.catch(noop);
88
157
  try {
89
158
  await writePaced(session, pcm, signal);
90
- await drainUntilIdle(() => lastPieceAt, () => streamClosed, signal);
91
159
  if (!signal?.aborted) {
92
160
  session.end();
93
161
  }
@@ -3,7 +3,7 @@ import { type Transport } from './rpc-session.ts';
3
3
  import { type KvCacheDeletionResult } from './capabilities/kv-cache.ts';
4
4
  import type { CustomModelEntry } from './models/model-catalog.ts';
5
5
  import { type ModelSourcePreview, type PreviewModelSourceContext } from './capabilities/preview-model-source.ts';
6
- import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, LoadModelContext, LoadModelResult, UnloadModelContext, UnloadModelResult, WarmModelContext } from './contract-types.ts';
6
+ import type { CapabilityInfo, CompletionContext, CredentialEntry, DetectedLanguage, DetectLanguageContext, DownloadContext, EmbedContext, HarnessContract, HarnessDiagnostics, HarnessEvent, HarnessInfo, InfraModel, InvokeContext, ListModelsOptions, ModelInfo, ResolvedSkillCatalogEntry, SkillsOptions, SkillWriteResult, SpeakContext, SpeakResult, SpeakStreamContext, ToolCatalogEntry, TranscribeContext, LoadModelContext, LoadModelResult, MemoryUsage, UnloadModelContext, UnloadModelResult, WarmModelContext, DownloadsAllowedOptions } from './contract-types.ts';
7
7
  import { type EmbedDevice } from './models/embed-config.ts';
8
8
  import type { DeleteModelSummary } from './models/model-delete.ts';
9
9
  import type { CopyBaseResult, DeleteOverlayResult, ManagedSkill, SkillFileContent } from './tools/skills/overlay-crud.ts';
@@ -36,6 +36,9 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
36
36
  setCredentials(entries: ReadonlyArray<CredentialEntry>): Promise<void>;
37
37
  setSkillSettings(entries: ReadonlyArray<SkillSettingEntry>): Promise<void>;
38
38
  setModelIdleTimeout(ms: number): Promise<void>;
39
+ setModelMemoryCap(percent: number): Promise<void>;
40
+ memoryUsage(): Promise<MemoryUsage>;
41
+ setDownloadsAllowed({ allowed }: DownloadsAllowedOptions): Promise<void>;
39
42
  listManagedSkills(): Promise<ManagedSkill[]>;
40
43
  readSkillFile(name: string): Promise<SkillFileContent | null>;
41
44
  writeSkillFile(name: string, content: string): Promise<SkillWriteResult>;
@@ -422,6 +422,18 @@ export class RemoteHarness extends EventEmitter {
422
422
  const { client } = await this._ensure();
423
423
  await client.setModelIdleTimeout({ ms });
424
424
  }
425
+ async setModelMemoryCap(percent) {
426
+ const { client } = await this._ensure();
427
+ await client.setModelMemoryCap({ percent });
428
+ }
429
+ async memoryUsage() {
430
+ const { client } = await this._ensure();
431
+ return await client.memoryUsage({});
432
+ }
433
+ async setDownloadsAllowed({ allowed }) {
434
+ const { client } = await this._ensure();
435
+ await client.setDownloadsAllowed({ allowed });
436
+ }
425
437
  async listManagedSkills() {
426
438
  const { client } = await this._ensure();
427
439
  const response = await client.listManagedSkills({});
@@ -566,10 +578,11 @@ export class RemoteHarness extends EventEmitter {
566
578
  cachedOnly: options.cachedOnly,
567
579
  fullRegistry: options.fullRegistry
568
580
  });
569
- return (response.models ?? []).map(({ actualSize, label, ...row }) => ({
581
+ return (response.models ?? []).map(({ actualSize, label, memoryBytes, ...row }) => ({
570
582
  ...row,
571
583
  actualSize: actualSize || undefined,
572
584
  label: label || undefined,
585
+ memoryBytes: memoryBytes || undefined,
573
586
  taskType: addonTaskType(row.type)
574
587
  }));
575
588
  }
@@ -95,6 +95,9 @@ export interface HarnessEvent {
95
95
  message?: string;
96
96
  [key: string]: unknown;
97
97
  }
98
+ export interface DownloadsAllowedOptions {
99
+ allowed: boolean;
100
+ }
98
101
  export interface ModelState {
99
102
  status: string;
100
103
  model?: string;
@@ -107,6 +110,7 @@ export interface ModelState {
107
110
  model: string;
108
111
  refs: number;
109
112
  }>;
113
+ addon?: string | null;
110
114
  }
111
115
  export interface WarmModelContext {
112
116
  model: string;
@@ -118,6 +122,9 @@ export interface LoadModelResult {
118
122
  loaded: boolean;
119
123
  reason?: 'memory';
120
124
  }
125
+ export interface MemoryUsage {
126
+ readonly usedBytes: number;
127
+ }
121
128
  export interface UnloadModelContext {
122
129
  model: string;
123
130
  }
@@ -223,6 +230,9 @@ export interface HarnessContract {
223
230
  agentId?: string | null;
224
231
  }>): void | Promise<void>;
225
232
  setModelIdleTimeout?(ms: number): void | Promise<void>;
233
+ setModelMemoryCap?(percent: number): void | Promise<void>;
234
+ memoryUsage?(): Promise<MemoryUsage>;
235
+ setDownloadsAllowed?(options: DownloadsAllowedOptions): void | Promise<void>;
226
236
  listManagedSkills?(): Promise<ManagedSkill[]>;
227
237
  readSkillFile?(name: string): Promise<SkillFileContent | null>;
228
238
  writeSkillFile?(name: string, content: string): Promise<{
@@ -0,0 +1,20 @@
1
+ import type { AbortSignal as BareAbortSignal } from 'bare-abort-controller';
2
+ import type { ModelAddon } from './model-source.ts';
3
+ export interface HeldModel {
4
+ readonly model: string;
5
+ readonly addon: ModelAddon;
6
+ }
7
+ export interface DownloadGateOptions {
8
+ isCached(model: string, addon: ModelAddon): Promise<boolean>;
9
+ onHeld(held: HeldModel): void;
10
+ onDropped(held: HeldModel): void;
11
+ }
12
+ export type HoldFn = (model: string, addon: ModelAddon, signal?: BareAbortSignal) => Promise<void>;
13
+ export interface DownloadGate {
14
+ readonly allowed: boolean;
15
+ setAllowed(allowed: boolean): void;
16
+ downloaded(): void;
17
+ hold: HoldFn;
18
+ abandon(): void;
19
+ }
20
+ export declare function createDownloadGate({ isCached, onHeld, onDropped }: DownloadGateOptions): DownloadGate;
@@ -0,0 +1,73 @@
1
+ import { AbortedError, raceAbort } from '../helpers/race-abort.js';
2
+ function deferred() {
3
+ const next = Promise.withResolvers();
4
+ next.promise.catch(noop);
5
+ return next;
6
+ }
7
+ export function createDownloadGate({ isCached, onHeld, onDropped }) {
8
+ let allowed = true;
9
+ let woke = deferred();
10
+ let dropped = deferred();
11
+ const wake = () => {
12
+ const woken = woke;
13
+ woke = deferred();
14
+ woken.resolve();
15
+ };
16
+ const wait = async (held, signal, onWait) => {
17
+ const drop = dropped;
18
+ const cancelled = () => signal?.aborted === true || drop !== dropped;
19
+ for (;;) {
20
+ const change = Promise.race([raceAbort(woke.promise, signal), drop.promise]);
21
+ change.catch(noop);
22
+ const ready = allowed || (await isCached(held.model, held.addon).catch(() => false));
23
+ if (cancelled()) {
24
+ throw new AbortedError();
25
+ }
26
+ if (ready) {
27
+ return;
28
+ }
29
+ onWait();
30
+ await change;
31
+ }
32
+ };
33
+ return {
34
+ get allowed() {
35
+ return allowed;
36
+ },
37
+ setAllowed(next) {
38
+ allowed = next;
39
+ if (allowed) {
40
+ wake();
41
+ }
42
+ },
43
+ downloaded: wake,
44
+ async hold(model, addon, signal) {
45
+ if (allowed)
46
+ return;
47
+ if (signal?.aborted)
48
+ throw new AbortedError();
49
+ const held = { model, addon };
50
+ let shown = false;
51
+ try {
52
+ await wait(held, signal, () => {
53
+ if (!shown) {
54
+ shown = true;
55
+ onHeld(held);
56
+ }
57
+ });
58
+ }
59
+ catch (err) {
60
+ if (shown) {
61
+ onDropped(held);
62
+ }
63
+ throw err;
64
+ }
65
+ },
66
+ abandon() {
67
+ const abandoned = dropped;
68
+ dropped = deferred();
69
+ abandoned.reject(new AbortedError());
70
+ }
71
+ };
72
+ }
73
+ function noop() { }