@qvac/harness 0.1.2 → 0.2.1

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 (38) hide show
  1. package/dist/harness.bundle +827 -6748
  2. package/dist/prebuilds-manifest.json +0 -7
  3. package/dist-lib/index.d.ts +1 -0
  4. package/dist-lib/index.js +21 -4
  5. package/dist-lib/lib/audio-transcode-bare.js +20 -20
  6. package/dist-lib/lib/capabilities/completion.d.ts +6 -0
  7. package/dist-lib/lib/capabilities/completion.js +48 -15
  8. package/dist-lib/lib/capabilities/download.d.ts +2 -0
  9. package/dist-lib/lib/capabilities/download.js +12 -3
  10. package/dist-lib/lib/capabilities/kv-cache.d.ts +1 -0
  11. package/dist-lib/lib/capabilities/kv-cache.js +7 -1
  12. package/dist-lib/lib/capabilities/tool-prompt-log.d.ts +23 -0
  13. package/dist-lib/lib/capabilities/tool-prompt-log.js +62 -0
  14. package/dist-lib/lib/capabilities/tool-selection.d.ts +2 -1
  15. package/dist-lib/lib/capabilities/tool-selection.js +2 -2
  16. package/dist-lib/lib/connect.d.ts +2 -0
  17. package/dist-lib/lib/connect.js +7 -1
  18. package/dist-lib/lib/contract-types.d.ts +1 -0
  19. package/dist-lib/lib/models/model-pool.d.ts +1 -1
  20. package/dist-lib/lib/sdk-loader.d.ts +1 -1
  21. package/dist-lib/lib/sdk-loader.js +1 -1
  22. package/dist-lib/lib/serve.js +2 -0
  23. package/dist-lib/lib/tools/exec.js +60 -8
  24. package/dist-lib/lib/tools/generate-image.d.ts +2 -1
  25. package/dist-lib/lib/tools/generate-image.js +2 -1
  26. package/dist-lib/lib/tools/skill.js +16 -0
  27. package/dist-lib/lib/wire.d.ts +1 -0
  28. package/dist-lib/lib/wire.js +6 -2
  29. package/dist-lib/schema/harness.js +12 -2
  30. package/dist-lib/spec/bind.js +3 -0
  31. package/dist-lib/spec/calls.js +1 -0
  32. package/dist-lib/spec/hrpc/hrpc.json +13 -0
  33. package/dist-lib/spec/hrpc/index.js +15 -3
  34. package/dist-lib/spec/hrpc/messages.js +37 -7
  35. package/dist-lib/spec/hyperschema/index.js +37 -7
  36. package/dist-lib/spec/hyperschema/schema.json +24 -1
  37. package/dist-lib/spec/hyperschema/types.d.ts +8 -2
  38. package/package.json +20 -12
@@ -1,12 +1,5 @@
1
1
  {
2
2
  "packages": {
3
- "@qvac/audiogen-ggml": [
4
- "darwin-arm64",
5
- "darwin-x64",
6
- "linux-arm64",
7
- "linux-x64",
8
- "win32-x64"
9
- ],
10
3
  "bare-abort": [
11
4
  "darwin-arm64",
12
5
  "darwin-x64",
@@ -224,6 +224,7 @@ export declare class Harness extends ReadyResource {
224
224
  list(options?: ListModelsOptions): Promise<ModelInfo[]>;
225
225
  deleteModel(name: string): Promise<DeleteModelSummary>;
226
226
  clearKvCache(): Promise<KvCacheDeletionResult>;
227
+ deleteKvCache(kvCacheKey: string): Promise<KvCacheDeletionResult>;
227
228
  infraModels(profile: string): Promise<Array<{
228
229
  name: string;
229
230
  addon: string;
package/dist-lib/index.js CHANGED
@@ -8,7 +8,7 @@ import { closePythonWorker } from "./lib/tools/exec/adapters/python/worker/clien
8
8
  import { completion } from "./lib/capabilities/completion.js";
9
9
  import { download } from "./lib/capabilities/download.js";
10
10
  import { createEmbedLane } from "./lib/capabilities/embed.js";
11
- import { deleteAllKvCaches } from "./lib/capabilities/kv-cache.js";
11
+ import { deleteAllKvCaches, deleteKvCacheKey } from "./lib/capabilities/kv-cache.js";
12
12
  import { pcmForTranscription } from "./lib/capabilities/audio-resample.js";
13
13
  import { buildWavBuffer } from "./lib/wav-encode.js";
14
14
  import { createVoiceLane, packSamples, speakOnce, speakStreamOnce, transcribeOnce, transcribeStreaming, SUPERTONIC_SAMPLE_RATE } from "./lib/capabilities/voice.js";
@@ -137,6 +137,7 @@ export class Harness extends ReadyResource {
137
137
  device: embedConfig.device,
138
138
  gpuLayers: embedConfig.gpuLayers
139
139
  });
140
+ const imageTransfers = new Map();
140
141
  const image = typeof imageModel === 'string' ? { model: imageModel } : imageModel;
141
142
  this._imageRuntime = image
142
143
  ? createDiffusionRuntime({
@@ -145,10 +146,22 @@ export class Harness extends ReadyResource {
145
146
  loadSdk: () => this._loadDiffusionSdk(),
146
147
  onProgress: (model, progress) => {
147
148
  const state = progressState(model, progress);
148
- if (state.status === 'loading' && state.percentage === 100) {
149
- invalidateModelStatusCache(this._modelStatus);
149
+ if (state.status === 'downloading' &&
150
+ typeof progress.percentage === 'number' &&
151
+ progress.percentage < 100) {
152
+ imageTransfers.set(model, state);
150
153
  }
151
- this.emit('model', state);
154
+ this.emit('model', state.status === 'downloading' && !imageTransfers.has(model)
155
+ ? { ...state, status: 'loading' }
156
+ : state);
157
+ },
158
+ onLoaded: (model) => {
159
+ const transfer = imageTransfers.get(model);
160
+ if (!transfer)
161
+ return;
162
+ imageTransfers.delete(model);
163
+ invalidateModelStatusCache(this._modelStatus);
164
+ this.emit('model', { ...transfer, status: 'downloaded' });
152
165
  },
153
166
  onBeforeLoad: async (signal) => {
154
167
  if (this._models.destroyed)
@@ -852,6 +865,10 @@ export class Harness extends ReadyResource {
852
865
  await this.ready();
853
866
  return deleteAllKvCaches(() => this._sdk.load());
854
867
  }
868
+ async deleteKvCache(kvCacheKey) {
869
+ await this.ready();
870
+ return deleteKvCacheKey(() => this._sdk.load(), kvCacheKey);
871
+ }
855
872
  async infraModels(profile) {
856
873
  return infraModelsFor(profile);
857
874
  }
@@ -32,11 +32,11 @@ function bufferReadIO(input) {
32
32
  }
33
33
  });
34
34
  }
35
- function makeOutFrame(capacity) {
35
+ function makeOutFrame(capacity, rate) {
36
36
  const out = new ffmpeg.Frame();
37
37
  out.format = ffmpeg.constants.sampleFormats.S16;
38
38
  out.channelLayout = ffmpeg.constants.channelLayouts.MONO;
39
- out.sampleRate = TARGET_SAMPLE_RATE;
39
+ out.sampleRate = rate;
40
40
  out.nbSamples = capacity;
41
41
  out.alloc();
42
42
  return out;
@@ -47,23 +47,23 @@ function readChunk(samples, outFrame, produced) {
47
47
  samples.read(outFrame);
48
48
  return Buffer.from(samples.data.subarray(0, produced * 2));
49
49
  }
50
- function createResampler(frame) {
51
- return new ffmpeg.Resampler(frame.sampleRate, frame.channelLayout, frame.format, TARGET_SAMPLE_RATE, ffmpeg.constants.channelLayouts.MONO, ffmpeg.constants.sampleFormats.S16);
50
+ function createResampler(frame, rate) {
51
+ return new ffmpeg.Resampler(frame.sampleRate, frame.channelLayout, frame.format, rate, ffmpeg.constants.channelLayouts.MONO, ffmpeg.constants.sampleFormats.S16);
52
52
  }
53
- function convertFrame(resampler, frame, samples) {
54
- const capacity = Math.ceil((frame.nbSamples * TARGET_SAMPLE_RATE) / frame.sampleRate) + 1024;
55
- const outFrame = makeOutFrame(capacity);
53
+ function convertFrame(resampler, frame, samples, rate) {
54
+ const capacity = Math.ceil((frame.nbSamples * rate) / frame.sampleRate) + 1024;
55
+ const outFrame = makeOutFrame(capacity, rate);
56
56
  const chunk = readChunk(samples, outFrame, resampler.convert(frame, outFrame));
57
57
  outFrame.destroy();
58
58
  return chunk;
59
59
  }
60
- function flushResampler(resampler, samples) {
60
+ function flushResampler(resampler, samples, rate) {
61
61
  if (!resampler)
62
62
  return [];
63
63
  const chunks = [];
64
64
  let flushed = 0;
65
65
  do {
66
- const outFrame = makeOutFrame(TARGET_SAMPLE_RATE);
66
+ const outFrame = makeOutFrame(rate, rate);
67
67
  flushed = resampler.flush(outFrame);
68
68
  const chunk = readChunk(samples, outFrame, flushed);
69
69
  if (chunk)
@@ -72,11 +72,11 @@ function flushResampler(resampler, samples) {
72
72
  } while (flushed > 0);
73
73
  return chunks;
74
74
  }
75
- function drainDecodedFrames(decoder, frame, samples, resampler, chunks) {
75
+ function drainDecodedFrames(decoder, frame, samples, resampler, chunks, rate) {
76
76
  let current = resampler;
77
77
  while (decoder.receiveFrame(frame)) {
78
- current ??= createResampler(frame);
79
- const chunk = convertFrame(current, frame, samples);
78
+ current ??= createResampler(frame, rate);
79
+ const chunk = convertFrame(current, frame, samples, rate);
80
80
  if (chunk)
81
81
  chunks.push(chunk);
82
82
  }
@@ -86,7 +86,7 @@ const YIELD_EVERY_PACKETS = 32;
86
86
  function yieldToLoop() {
87
87
  return new Promise((resolve) => setTimeout(resolve, 0));
88
88
  }
89
- async function decodeToPcmChunks(inputFormat, stream, decoder) {
89
+ async function decodeToPcmChunks(inputFormat, stream, decoder, rate) {
90
90
  const chunks = [];
91
91
  const packet = new ffmpeg.Packet();
92
92
  const flush = new ffmpeg.Packet();
@@ -97,15 +97,15 @@ async function decodeToPcmChunks(inputFormat, stream, decoder) {
97
97
  try {
98
98
  while (inputFormat.readFrame(packet)) {
99
99
  if (packet.streamIndex === stream.index && decoder.sendPacket(packet)) {
100
- resampler = drainDecodedFrames(decoder, frame, samples, resampler, chunks);
100
+ resampler = drainDecodedFrames(decoder, frame, samples, resampler, chunks, rate);
101
101
  }
102
102
  packet.unref();
103
103
  if (++packets % YIELD_EVERY_PACKETS === 0)
104
104
  await yieldToLoop();
105
105
  }
106
106
  decoder.sendPacket(flush);
107
- resampler = drainDecodedFrames(decoder, frame, samples, resampler, chunks);
108
- chunks.push(...flushResampler(resampler, samples));
107
+ resampler = drainDecodedFrames(decoder, frame, samples, resampler, chunks, rate);
108
+ chunks.push(...flushResampler(resampler, samples, rate));
109
109
  }
110
110
  finally {
111
111
  resampler?.destroy();
@@ -115,7 +115,7 @@ async function decodeToPcmChunks(inputFormat, stream, decoder) {
115
115
  }
116
116
  return chunks;
117
117
  }
118
- async function decodeInputToPcm(input) {
118
+ async function decodeInputToPcm(input, rate) {
119
119
  let inputFormat = null;
120
120
  let decoder = null;
121
121
  try {
@@ -123,7 +123,7 @@ async function decodeInputToPcm(input) {
123
123
  const stream = inputFormat.getBestStream(ffmpeg.constants.mediaTypes.AUDIO);
124
124
  decoder = stream.decoder();
125
125
  decoder.open();
126
- const chunks = await decodeToPcmChunks(inputFormat, stream, decoder);
126
+ const chunks = await decodeToPcmChunks(inputFormat, stream, decoder, rate);
127
127
  return chunks.length === 0 ? null : Buffer.concat(chunks);
128
128
  }
129
129
  catch (e) {
@@ -137,9 +137,9 @@ async function decodeInputToPcm(input) {
137
137
  }
138
138
  }
139
139
  export async function transcodeToWav16kMono(input) {
140
- const pcm = await decodeInputToPcm(input);
140
+ const pcm = await decodeInputToPcm(input, TARGET_SAMPLE_RATE);
141
141
  return pcm ? buildWavBuffer(pcm) : null;
142
142
  }
143
143
  export async function transcodeToPcm16kMono(input) {
144
- return decodeInputToPcm(input);
144
+ return decodeInputToPcm(input, TARGET_SAMPLE_RATE);
145
145
  }
@@ -159,10 +159,15 @@ export interface ToolParameters {
159
159
  properties: Record<string, ToolParameterSchema>;
160
160
  required?: string[];
161
161
  }
162
+ export interface ToolPromptDefinition {
163
+ description?: string;
164
+ parameters?: ToolParameters;
165
+ }
162
166
  export interface ToolDefinition {
163
167
  name: string;
164
168
  description: string;
165
169
  parameters: ToolParameters;
170
+ prompt?: ToolPromptDefinition;
166
171
  autoApprove?: boolean;
167
172
  skillGated?: boolean;
168
173
  rootsRequired?: boolean;
@@ -279,6 +284,7 @@ interface CompletionArgs {
279
284
  fsRoots?: readonly DriveRoot[];
280
285
  fsWriteRoots?: readonly DriveRoot[];
281
286
  }
287
+ export declare const CONTEXT_OVERFLOW_MESSAGE = "This message is too large for the model context window.";
282
288
  export declare function trustedSourcesFrom(history: readonly Message[]): TrustedSource[];
283
289
  export declare function toSdkTool(tool: ToolDefinition): SdkTool;
284
290
  export type CompletionSkillEntry = SkillCatalogEntry & {
@@ -26,6 +26,7 @@ import { isAvailableState } from "../tools/skills/skill-state-constants.js";
26
26
  import { resolveImplicitSkill, resolveSkillMention, skillPreloadMessage } from "../tools/skills/mention.js";
27
27
  import { permittedTools } from "../tools/toolsets.js";
28
28
  import { promptPrefixHash } from "./prompt-prefix.js";
29
+ import { logToolPrompt } from "./tool-prompt-log.js";
29
30
  import { DIRECT_CALL_ID, loadedSkillFilesFrom, loadedSkillsFrom, TOOL_CONTINUATION_NUDGE, toolResultTurn, withoutSkillLoadTurns } from "./tool-turns.js";
30
31
  import { projectVisionHistory, visionHistoryCacheKey } from "./vision-history.js";
31
32
  export function imageSettingsFrom(agent) {
@@ -49,7 +50,7 @@ function runWallClockBudgetMs() {
49
50
  const ms = Number(process.env.QVAC_RUN_BUDGET_MS);
50
51
  return ms > 0 ? ms : RUN_WALL_CLOCK_BUDGET_MS;
51
52
  }
52
- const CONTEXT_OVERFLOW_MESSAGE = 'This message is too large for the model context window.';
53
+ export const CONTEXT_OVERFLOW_MESSAGE = 'This message is too large for the model context window.';
53
54
  const MAX_MUTE_ROUNDS = 1;
54
55
  const NARRATED_INTENT = /\b(I['’]ll|I will|Now I|Let me(?! know)|Next,? I)\b[^.!?]*[.!?]?\s*$/i;
55
56
  const MAX_WEB_SEARCHES_PER_TURN = 3;
@@ -72,25 +73,25 @@ function originOfToolTurn(text) {
72
73
  return text.startsWith(`[Tool: ${SKILL_TOOL_NAME} (`) ? 'skill' : 'tool-result';
73
74
  }
74
75
  const noop = () => { };
75
- function intentParameters(tool) {
76
- if (tool.autoApprove === true || INTENT_PARAM in tool.parameters.properties) {
77
- return tool.parameters;
76
+ function intentParameters(tool, params) {
77
+ if (tool.autoApprove === true || INTENT_PARAM in params.properties) {
78
+ return params;
78
79
  }
79
80
  return {
80
- ...tool.parameters,
81
+ ...params,
81
82
  properties: {
82
83
  [INTENT_PARAM]: { type: 'string', description: INTENT_PARAM_DESCRIPTION },
83
- ...tool.parameters.properties
84
+ ...params.properties
84
85
  },
85
- required: [INTENT_PARAM, ...(tool.parameters.required ?? [])]
86
+ required: [INTENT_PARAM, ...(params.required ?? [])]
86
87
  };
87
88
  }
88
89
  export function toSdkTool(tool) {
89
90
  return {
90
91
  type: 'function',
91
92
  name: tool.name,
92
- description: tool.description,
93
- parameters: intentParameters(tool)
93
+ description: tool.prompt?.description ?? tool.description,
94
+ parameters: intentParameters(tool, tool.prompt?.parameters ?? tool.parameters)
94
95
  };
95
96
  }
96
97
  function sanitizeTurn(turn) {
@@ -597,18 +598,21 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
597
598
  };
598
599
  const activeSkills = inContextSkills.filter((skill) => skillIsActive(skill.name, activation));
599
600
  const { grantedNames, grantedOwners, grantedOps } = collectToolGrants(activeSkills);
600
- const enabled = responseFormat
601
- ? []
602
- : enabledTools(tools, {
601
+ function selectEnabled() {
602
+ if (responseFormat)
603
+ return [];
604
+ const loaded = activeSkills.filter((skill) => loadedSkills.has(skill.name));
605
+ return enabledTools(tools, {
603
606
  toolsSelected,
604
607
  allowlist: agent?.enabledTools ?? [],
605
608
  grantedNames,
606
609
  grantedOwners,
610
+ loadedGrantedNames: collectToolGrants(loaded).grantedNames,
607
611
  hasRoots: fsRoots.length > 0,
608
612
  hasWriteRoots: writableRoots.length > 0,
609
613
  hasActiveSkills: activeSkills.length > 0
610
614
  });
611
- const sdkTools = enabled.length ? enabled.map((tool) => toSdkTool(tool)) : undefined;
615
+ }
612
616
  const reasoningEnabled = agent?.reasoning === 'enabled' ? true : agent?.reasoning === 'disabled' ? false : undefined;
613
617
  const deviceReasoning = thinkingDefault === 'enabled' ? true : thinkingDefault === 'disabled' ? false : undefined;
614
618
  const resolvedGenerationParams = resolveLlmGenerationParams(model, reasoningEnabled, deviceReasoning);
@@ -672,14 +676,35 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
672
676
  loadedThisTurn.add(preload.name);
673
677
  skillIcon = skillEmoji(preload.name) ?? skillIcon;
674
678
  }
679
+ let enabled = selectEnabled();
680
+ let sdkTools = enabled.length ? enabled.map((tool) => toSdkTool(tool)) : undefined;
675
681
  const system = systemPromptParts({
676
682
  bio: agent?.bio,
677
683
  toolNames: enabled.map((tool) => tool.name),
678
684
  skillsSection: skillsPromptSection(activeSkills)
679
685
  });
680
686
  const systemContent = system.text;
681
- const promptPrefix = promptPrefixHash({ system: systemContent, tools: sdkTools });
682
- const promptParts = promptUsageParts({ system, toolSchemas: sdkTools });
687
+ let promptPrefix = promptPrefixHash({ system: systemContent, tools: sdkTools });
688
+ let promptParts = promptUsageParts({ system, toolSchemas: sdkTools });
689
+ function logSelection() {
690
+ logToolPrompt({
691
+ promptPrefix,
692
+ modelId,
693
+ cacheKey,
694
+ agentId: agent?.id,
695
+ messageCount: messages.length,
696
+ responseFormat: responseFormat?.type,
697
+ tools: sdkTools,
698
+ definitions: enabled,
699
+ activeSkills: activeSkills.map((skill) => skill.name),
700
+ skillsSelected,
701
+ enabledSkills: agent?.enabledSkills ?? [],
702
+ mentionedSkill: mention?.name ?? null,
703
+ loadedSkills,
704
+ toolOps: grantedOps
705
+ });
706
+ }
707
+ logSelection();
683
708
  let history = annotateLastUserTime(systemContent ? [{ role: 'system', content: systemContent }, ...turns] : turns);
684
709
  let roundCacheKey = stubDropped ? undefined : visionHistoryCacheKey(cacheKey, history);
685
710
  const runUsage = () => usageBreakdown(promptParts, stats.cacheTokens, model);
@@ -771,6 +796,14 @@ export async function* completion(sdk, { modelId, acquireRound, model, agent, ct
771
796
  outcome = 'time-capped';
772
797
  break;
773
798
  }
799
+ const reselected = selectEnabled();
800
+ if (reselected.map((tool) => tool.name).join() !== enabled.map((tool) => tool.name).join()) {
801
+ enabled = reselected;
802
+ sdkTools = enabled.length ? enabled.map((tool) => toSdkTool(tool)) : undefined;
803
+ promptPrefix = promptPrefixHash({ system: systemContent, tools: sdkTools });
804
+ promptParts = promptUsageParts({ system, toolSchemas: sdkTools });
805
+ logSelection();
806
+ }
774
807
  hlog('round start', {
775
808
  round,
776
809
  turns: history.length,
@@ -5,10 +5,12 @@ import { type ModelAddon, type ModelRegistrySdk } from '../models/model-source.t
5
5
  export type DownloadProgress = Omit<ModelProgressUpdate, 'type'>;
6
6
  export type DownloadProgressEvent = {
7
7
  type: 'progress';
8
+ transferred?: boolean;
8
9
  } & DownloadProgress;
9
10
  export type DownloadCompleteEvent = {
10
11
  type: 'complete';
11
12
  assetId: string;
13
+ downloaded?: boolean;
12
14
  };
13
15
  export type DownloadErrorEvent = {
14
16
  type: 'error';
@@ -5,6 +5,7 @@ const noop = () => { };
5
5
  export function download(sdkOrLoader, args, onSettled) {
6
6
  const { model, addon = 'llm', seed, signal, projection, companions = [], assetSizes } = args;
7
7
  const loadSdk = typeof sdkOrLoader === 'function' ? sdkOrLoader : () => Promise.resolve(sdkOrLoader);
8
+ let transferred = false;
8
9
  let settled = false;
9
10
  let opDone = false;
10
11
  let cancelSent = false;
@@ -30,7 +31,7 @@ export function download(sdkOrLoader, args, onSettled) {
30
31
  };
31
32
  const onProgress = (p) => {
32
33
  if (!settled)
33
- stream.push({ ...p, type: 'progress' });
34
+ stream.push({ ...p, type: 'progress', transferred });
34
35
  };
35
36
  const start = async () => {
36
37
  if (signal?.aborted)
@@ -56,14 +57,22 @@ export function download(sdkOrLoader, args, onSettled) {
56
57
  totalBytes: aggregate.totalBytes
57
58
  })
58
59
  : onProgress;
59
- const op = sdkRef.downloadAsset({ assetSrc: srcs[i], seed, onProgress: progress });
60
+ const op = sdkRef.downloadAsset({
61
+ assetSrc: srcs[i],
62
+ seed,
63
+ onProgress: (update) => {
64
+ if (update.percentage < 100)
65
+ transferred = true;
66
+ progress(update);
67
+ }
68
+ });
60
69
  requestId = op.requestId;
61
70
  const assetId = await op;
62
71
  if (i === 0)
63
72
  mainAssetId = assetId;
64
73
  }
65
74
  opDone = true;
66
- finish({ type: 'complete', assetId: mainAssetId });
75
+ finish({ type: 'complete', assetId: mainAssetId, downloaded: transferred });
67
76
  }
68
77
  catch (err) {
69
78
  opDone = true;
@@ -9,5 +9,6 @@ export type KvCacheDeletionResult = {
9
9
  };
10
10
  type KvCacheSdk = Pick<typeof import('@qvac/sdk'), 'deleteCache'>;
11
11
  export declare function deleteAllKvCaches(loadSdk: () => Promise<KvCacheSdk>): Promise<KvCacheDeletionResult>;
12
+ export declare function deleteKvCacheKey(loadSdk: () => Promise<KvCacheSdk>, kvCacheKey: string): Promise<KvCacheDeletionResult>;
12
13
  export declare function unsupportedKvCacheDeletion(error?: string): KvCacheDeletionResult;
13
14
  export {};
@@ -1,7 +1,13 @@
1
1
  export async function deleteAllKvCaches(loadSdk) {
2
+ return deleteCacheWith(loadSdk, { all: true });
3
+ }
4
+ export async function deleteKvCacheKey(loadSdk, kvCacheKey) {
5
+ return deleteCacheWith(loadSdk, { kvCacheKey });
6
+ }
7
+ async function deleteCacheWith(loadSdk, params) {
2
8
  try {
3
9
  const sdk = await loadSdk();
4
- const result = await sdk.deleteCache({ all: true });
10
+ const result = await sdk.deleteCache(params);
5
11
  return result.success
6
12
  ? { status: 'success' }
7
13
  : { status: 'failure', error: 'SDK cache deletion did not succeed' };
@@ -0,0 +1,23 @@
1
+ import type { Tool as SdkTool } from '@qvac/sdk';
2
+ export interface ToolOwnership {
3
+ readonly name: string;
4
+ readonly skillGated?: boolean | undefined;
5
+ readonly owningSkill?: string | undefined;
6
+ }
7
+ export interface ToolPromptLogOptions {
8
+ readonly promptPrefix: string;
9
+ readonly modelId: string;
10
+ readonly cacheKey: string | undefined;
11
+ readonly agentId: string | undefined;
12
+ readonly messageCount: number;
13
+ readonly responseFormat: string | undefined;
14
+ readonly tools: ReadonlyArray<SdkTool> | undefined;
15
+ readonly definitions: ReadonlyArray<ToolOwnership>;
16
+ readonly activeSkills: ReadonlyArray<string>;
17
+ readonly skillsSelected: boolean;
18
+ readonly enabledSkills: ReadonlyArray<string>;
19
+ readonly mentionedSkill: string | null;
20
+ readonly loadedSkills: ReadonlySet<string>;
21
+ readonly toolOps: ReadonlyMap<string, ReadonlyArray<unknown>>;
22
+ }
23
+ export declare function logToolPrompt({ promptPrefix, modelId, cacheKey, agentId, messageCount, responseFormat, tools, definitions, activeSkills, skillsSelected, enabledSkills, mentionedSkill, loadedSkills, toolOps }: ToolPromptLogOptions): void;
@@ -0,0 +1,62 @@
1
+ import { harnessLogEnabled, hlog } from "../log.js";
2
+ export function logToolPrompt({ promptPrefix, modelId, cacheKey, agentId, messageCount, responseFormat, tools = [], definitions, activeSkills, skillsSelected, enabledSkills, mentionedSkill, loadedSkills, toolOps }) {
3
+ if (!harnessLogEnabled())
4
+ return;
5
+ hlog('sdk tool definitions', {
6
+ promptPrefix,
7
+ modelId,
8
+ cacheKey: cacheKey ?? null,
9
+ agentId: agentId ?? null,
10
+ messageCount,
11
+ responseFormat: responseFormat ?? 'text',
12
+ schemaChars: tools.length ? JSON.stringify(tools).length : 0,
13
+ tools: rankedTools(tools, definitions),
14
+ skillGatedChars: skillGatedChars(tools, definitions),
15
+ activeSkills,
16
+ skillActivation: {
17
+ scope: skillsSelected ? 'selected' : 'all',
18
+ enabled: enabledSkills,
19
+ mentioned: mentionedSkill,
20
+ loaded: [...loadedSkills]
21
+ },
22
+ localCliValidators: [...toolOps].map(([bin, validators]) => ({
23
+ bin,
24
+ count: validators.length
25
+ }))
26
+ });
27
+ }
28
+ function ownerOf(definitions, name) {
29
+ return definitions.find((definition) => definition.name === name);
30
+ }
31
+ function rankedTools(tools, definitions) {
32
+ return tools
33
+ .map((tool) => {
34
+ const owner = ownerOf(definitions, tool.name);
35
+ return {
36
+ name: tool.name,
37
+ schemaChars: JSON.stringify(tool).length,
38
+ descriptionChars: descriptionChars(tool),
39
+ parameters: Object.keys(tool.parameters.properties),
40
+ required: tool.parameters.required ?? [],
41
+ skillGated: owner?.skillGated === true,
42
+ owningSkill: owner?.owningSkill ?? null
43
+ };
44
+ })
45
+ .sort((a, b) => b.schemaChars - a.schemaChars);
46
+ }
47
+ function skillGatedChars(tools, definitions) {
48
+ let chars = 0;
49
+ for (const tool of tools) {
50
+ if (ownerOf(definitions, tool.name)?.skillGated !== true)
51
+ continue;
52
+ chars += JSON.stringify(tool).length;
53
+ }
54
+ return chars;
55
+ }
56
+ function descriptionChars(tool) {
57
+ let chars = tool.description.length;
58
+ for (const parameter of Object.values(tool.parameters.properties)) {
59
+ chars += parameter.description?.length ?? 0;
60
+ }
61
+ return chars;
62
+ }
@@ -35,9 +35,10 @@ export interface ToolGateInputs {
35
35
  allowlist: ReadonlyArray<string>;
36
36
  grantedNames: ReadonlySet<string>;
37
37
  grantedOwners: ReadonlyMap<string, ReadonlySet<string>>;
38
+ loadedGrantedNames: ReadonlySet<string>;
38
39
  hasRoots: boolean;
39
40
  hasWriteRoots: boolean;
40
41
  hasActiveSkills: boolean;
41
42
  }
42
43
  export declare function isSelected(scope: 'all' | 'selected' | null | undefined, list?: ReadonlyArray<string> | null): boolean;
43
- export declare function enabledTools<T extends SelectableTool>(tools: ReadonlyArray<T>, { toolsSelected, allowlist, grantedNames, grantedOwners, hasRoots, hasWriteRoots, hasActiveSkills }: ToolGateInputs): T[];
44
+ export declare function enabledTools<T extends SelectableTool>(tools: ReadonlyArray<T>, { toolsSelected, allowlist, grantedNames, grantedOwners, loadedGrantedNames, hasRoots, hasWriteRoots, hasActiveSkills }: ToolGateInputs): T[];
@@ -35,7 +35,7 @@ export function skillIsActive(name, { skillsSelected, enabledSkills, mention, lo
35
35
  export function isSelected(scope, list) {
36
36
  return (scope ?? (list?.length ? 'selected' : 'all')) === 'selected';
37
37
  }
38
- export function enabledTools(tools, { toolsSelected, allowlist, grantedNames, grantedOwners, hasRoots, hasWriteRoots, hasActiveSkills }) {
38
+ export function enabledTools(tools, { toolsSelected, allowlist, grantedNames, grantedOwners, loadedGrantedNames, hasRoots, hasWriteRoots, hasActiveSkills }) {
39
39
  return tools.filter((tool) => {
40
40
  if (tool.name === SKILL_TOOL_NAME)
41
41
  return hasActiveSkills;
@@ -52,7 +52,7 @@ export function enabledTools(tools, { toolsSelected, allowlist, grantedNames, gr
52
52
  if (tool.writeRootsRequired)
53
53
  return hasWriteRoots;
54
54
  if (tool.skillGated)
55
- return grantedNames.has(tool.name);
55
+ return loadedGrantedNames.has(tool.name);
56
56
  if (!toolsSelected)
57
57
  return true;
58
58
  return allowlist.includes(tool.name) || grantedNames.has(tool.name);
@@ -65,6 +65,8 @@ export declare class RemoteHarness extends EventEmitter implements HarnessContra
65
65
  list(options?: ListModelsOptions): Promise<ModelInfo[]>;
66
66
  deleteModel(name: string): Promise<DeleteModelSummary>;
67
67
  clearKvCache(): Promise<KvCacheDeletionResult>;
68
+ deleteKvCache(kvCacheKey: string): Promise<KvCacheDeletionResult>;
69
+ private _deleteKvCaches;
68
70
  infraModels(profile: string): Promise<Array<{
69
71
  name: string;
70
72
  addon: string;
@@ -524,12 +524,18 @@ export class RemoteHarness extends EventEmitter {
524
524
  return { name: response.name, removed: response.removed, freedBytes: response.freedBytes };
525
525
  }
526
526
  async clearKvCache() {
527
+ return this._deleteKvCaches((client) => client.clearKvCache({}));
528
+ }
529
+ async deleteKvCache(kvCacheKey) {
530
+ return this._deleteKvCaches((client) => client.deleteKvCache({ kvCacheKey }));
531
+ }
532
+ async _deleteKvCaches(call) {
527
533
  try {
528
534
  const { client } = await this._ensure();
529
535
  const info = await client.describe({});
530
536
  if (info.supportsKvCacheDeletion !== true)
531
537
  return unsupportedKvCacheDeletion();
532
- const response = await client.clearKvCache({});
538
+ const response = await call(client);
533
539
  if (response.status === 'success')
534
540
  return { status: 'success' };
535
541
  if (response.status === 'failure') {
@@ -191,6 +191,7 @@ export interface HarnessContract {
191
191
  warmModel?(context: WarmModelContext): Promise<void>;
192
192
  deleteModel?(name: string): Promise<DeleteModelSummary>;
193
193
  clearKvCache?(): Promise<KvCacheDeletionResult>;
194
+ deleteKvCache?(kvCacheKey: string): Promise<KvCacheDeletionResult>;
194
195
  infraModels?(profile: string): Promise<ReadonlyArray<{
195
196
  name: string;
196
197
  addon: string;
@@ -5,7 +5,7 @@ import { type ModelAddon } from './model-source.ts';
5
5
  import type { GpuFaultState } from './sdk-faults.ts';
6
6
  type SdkModule = typeof import('@qvac/sdk');
7
7
  export interface ModelState {
8
- status: 'loading' | 'downloading' | 'ready' | 'unloading' | 'unloaded';
8
+ status: 'loading' | 'downloading' | 'downloaded' | 'ready' | 'unloading' | 'unloaded';
9
9
  model: string;
10
10
  [key: string]: unknown;
11
11
  }
@@ -1,4 +1,4 @@
1
- type Sdk = typeof import('@qvac/sdk');
1
+ type Sdk = typeof import('#sdk');
2
2
  export type SdkPlugin = Parameters<(typeof import('@qvac/sdk/plugins'))['registerPlugin']>[0];
3
3
  type Plugin = SdkPlugin;
4
4
  interface PluginRegistry {
@@ -1,7 +1,7 @@
1
1
  import { retryMemo } from "./helpers/retry-memo.js";
2
2
  import { hlog } from "./log.js";
3
3
  const defaultImports = {
4
- sdk: () => import('@qvac/sdk'),
4
+ sdk: () => import('#sdk'),
5
5
  plugins: () => import('@qvac/sdk/plugins'),
6
6
  llm: () => import('@qvac/sdk/llamacpp-completion/plugin'),
7
7
  embeddings: () => import('@qvac/sdk/llamacpp-embedding/plugin'),
@@ -16,6 +16,7 @@ import { packVectors, toDownloadWire, toWire } from "./wire.js";
16
16
  const MODEL_STATUSES = new Set([
17
17
  'loading',
18
18
  'downloading',
19
+ 'downloaded',
19
20
  'ready',
20
21
  'error',
21
22
  'unloading',
@@ -133,6 +134,7 @@ export function serveHarness(stream, harness) {
133
134
  return (await harness.deleteModel?.(name)) ?? { name, removed: 0, freedBytes: 0 };
134
135
  },
135
136
  clearKvCache: async () => (await harness.clearKvCache?.()) ?? unsupportedKvCacheDeletion(),
137
+ deleteKvCache: async ({ kvCacheKey }) => (await harness.deleteKvCache?.(kvCacheKey)) ?? unsupportedKvCacheDeletion(),
136
138
  infraModels: async ({ profile }) => ({
137
139
  models: (await harness.infraModels?.(profile ?? '')) ?? []
138
140
  }),