@qvac/harness 0.2.4 → 0.2.5

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.
@@ -272,6 +272,14 @@
272
272
  "win32-arm64",
273
273
  "win32-x64"
274
274
  ],
275
+ "bare-tty/node_modules/bare-signals": [
276
+ "darwin-arm64",
277
+ "darwin-x64",
278
+ "linux-arm64",
279
+ "linux-x64",
280
+ "win32-arm64",
281
+ "win32-x64"
282
+ ],
275
283
  "bare-tty": [
276
284
  "darwin-arm64",
277
285
  "darwin-x64",
@@ -63,6 +63,7 @@ export interface MusicModelOptions extends MusicModelStages {
63
63
  }
64
64
  export interface HarnessOptions {
65
65
  tools?: ToolDefinition[];
66
+ gpuStats?: unknown;
66
67
  sdkLoader?: SdkLoader;
67
68
  skillsDir?: string;
68
69
  projectionModel?: string | Record<string, string>;
@@ -131,6 +132,7 @@ export declare class Harness extends ReadyResource {
131
132
  private readonly _kvCacheK?;
132
133
  private readonly _kvCacheV?;
133
134
  private readonly _transcriptionDevice?;
135
+ private readonly _gpuStats?;
134
136
  private _allSkills;
135
137
  private _credentialStore?;
136
138
  private _credentials;
@@ -143,7 +145,7 @@ export declare class Harness extends ReadyResource {
143
145
  private _capability;
144
146
  private _capabilityInflight;
145
147
  private readonly _modelFaults;
146
- constructor({ tools, sdkLoader, skillsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, credentialStore, platform }?: HarnessOptions);
148
+ constructor({ tools, sdkLoader, skillsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, credentialStore, platform, gpuStats }?: HarnessOptions);
147
149
  _open(): Promise<void>;
148
150
  get suspended(): boolean;
149
151
  get skillNames(): string[];
package/dist-lib/index.js CHANGED
@@ -2,7 +2,7 @@ import fs from 'fs';
2
2
  import { platform, totalmem } from 'os';
3
3
  import process from 'process';
4
4
  import ReadyResource from 'ready-resource';
5
- import { readSystemStats } from '#hw-gpu-stats';
5
+ import { probeGpuStats } from "./lib/hardware/gpu-probe.js";
6
6
  import { probeCapabilityProfile } from "./lib/hardware/capability-probe.js";
7
7
  import { closePythonWorker } from "./lib/tools/exec/adapters/python/worker/client.js";
8
8
  import { completion } from "./lib/capabilities/completion.js";
@@ -103,6 +103,7 @@ export class Harness extends ReadyResource {
103
103
  _kvCacheK;
104
104
  _kvCacheV;
105
105
  _transcriptionDevice;
106
+ _gpuStats;
106
107
  _allSkills;
107
108
  _credentialStore;
108
109
  _credentials = new Map();
@@ -115,10 +116,11 @@ export class Harness extends ReadyResource {
115
116
  _capability = null;
116
117
  _capabilityInflight = null;
117
118
  _modelFaults = new Map();
118
- constructor({ tools = [], sdkLoader, skillsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, credentialStore, platform } = {}) {
119
+ constructor({ tools = [], sdkLoader, skillsDir, projectionModel, imageModel, imageClamps, musicModel, musicClamps, llmDevice, llmGpuLayers, contextSize, kvCacheK, kvCacheV, embedDevice, embedGpuLayers, embedBatchSize, transcriptionDevice, credentialStore, platform, gpuStats } = {}) {
119
120
  super();
120
121
  this._sdk = sdkLoader ?? createSdkLoader();
121
122
  this._llmDevice = llmDevice;
123
+ this._gpuStats = gpuStats;
122
124
  this._llmGpuLayers = llmGpuLayers;
123
125
  this._contextSize = contextSize;
124
126
  this._kvCacheK = kvCacheK;
@@ -898,7 +900,7 @@ export class Harness extends ReadyResource {
898
900
  return null;
899
901
  }
900
902
  },
901
- readGpuStats: () => readSystemStats()
903
+ readGpuStats: () => (this._gpuStats !== undefined ? this._gpuStats : probeGpuStats())
902
904
  };
903
905
  this._capabilityInflight = probeCapabilityProfile(reads)
904
906
  .then((capability) => {
@@ -4,6 +4,6 @@ export interface PlatformReads {
4
4
  platform(): string;
5
5
  readFile(path: string): Promise<string | null>;
6
6
  run(command: string): Promise<string | null>;
7
- readGpuStats(): unknown;
7
+ readGpuStats(): unknown | Promise<unknown>;
8
8
  }
9
9
  export declare function probeCapabilityProfile(reads: PlatformReads): Promise<CapabilityInfo>;
@@ -49,7 +49,7 @@ async function probeSysfs(reads) {
49
49
  }
50
50
  async function probeDesktop(reads) {
51
51
  const ram = reads.totalmem();
52
- const addon = probeWithAddon(reads.readGpuStats(), ram);
52
+ const addon = probeWithAddon(await reads.readGpuStats(), ram);
53
53
  if (addon && addon.source === 'vram')
54
54
  return addon;
55
55
  const nvidia = await reads.run('nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits');
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import { runGpuProbeIfRequested } from "./gpu-probe.js";
2
+ runGpuProbeIfRequested();
@@ -0,0 +1,8 @@
1
+ export interface GpuProbeResult {
2
+ readonly ok: boolean;
3
+ readonly stats: unknown;
4
+ }
5
+ export declare function runGpuProbeIfRequested(): void;
6
+ export declare function parseGpuProbeOutput(output: string): unknown;
7
+ export declare function spawnGpuProbe(entry?: string): Promise<GpuProbeResult>;
8
+ export declare function probeGpuStats(): Promise<unknown>;
@@ -0,0 +1,54 @@
1
+ import { spawn } from 'child_process';
2
+ import os from 'os';
3
+ import { readSystemStats } from '#hw-gpu-stats';
4
+ const FLAG = '--gpu-probe';
5
+ const TIMEOUT_MS = 5000;
6
+ export function runGpuProbeIfRequested() {
7
+ if (!Bare.argv.includes(FLAG))
8
+ return;
9
+ console.log(JSON.stringify(readSystemStats()));
10
+ queueMicrotask(() => Bare.exit(0));
11
+ }
12
+ export function parseGpuProbeOutput(output) {
13
+ const line = output.trim().split('\n').at(-1) ?? '';
14
+ if (line.length === 0)
15
+ return null;
16
+ try {
17
+ const parsed = JSON.parse(line);
18
+ return typeof parsed === 'object' ? parsed : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function spawnGpuProbe(entry = Bare.argv[1]) {
25
+ return new Promise((resolve) => {
26
+ const child = spawn(Bare.argv[0], [entry, FLAG], {
27
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe']
28
+ });
29
+ let output = '';
30
+ const timer = setTimeout(() => {
31
+ child.kill();
32
+ resolve({ ok: false, stats: null });
33
+ }, TIMEOUT_MS);
34
+ child.stdout?.on('data', (chunk) => {
35
+ output += Buffer.from(chunk).toString();
36
+ });
37
+ child.stderr?.on('data', () => { });
38
+ child.on('error', () => {
39
+ clearTimeout(timer);
40
+ resolve({ ok: false, stats: null });
41
+ });
42
+ child.on('exit', (code) => {
43
+ clearTimeout(timer);
44
+ resolve(code === 0 ? { ok: true, stats: parseGpuProbeOutput(output) } : { ok: false, stats: null });
45
+ });
46
+ });
47
+ }
48
+ export async function probeGpuStats() {
49
+ const platform = os.platform();
50
+ if (typeof Bare === 'undefined' || (platform !== 'linux' && platform !== 'win32')) {
51
+ return readSystemStats();
52
+ }
53
+ return (await spawnGpuProbe()).stats;
54
+ }
@@ -1 +1 @@
1
- export declare function readSystemStats(): unknown;
1
+ export { readSystemStats } from './gpu-stats.native.cjs';
@@ -1,24 +1 @@
1
- import os from 'os';
2
- import GPUInfo from 'bare-gpu-info';
3
- export function readSystemStats() {
4
- try {
5
- const info = new GPUInfo();
6
- try {
7
- const devices = [];
8
- for (const gpu of info) {
9
- if (typeof gpu.memory !== 'number' || !gpu.name)
10
- continue;
11
- devices.push({ name: gpu.name, videoMemoryBytes: gpu.memory });
12
- }
13
- if (devices.length === 0)
14
- return null;
15
- return { memory: { totalBytes: os.totalmem() }, gpu: { devices } };
16
- }
17
- finally {
18
- info.destroy();
19
- }
20
- }
21
- catch {
22
- return null;
23
- }
24
- }
1
+ export { readSystemStats } from './gpu-stats.native.cjs';
@@ -0,0 +1,27 @@
1
+ const os = require('os')
2
+
3
+ // loaded on first read: the addon drags the graphics stack (and on Linux a
4
+ // second LLVM) into whichever process imports it
5
+ let GPUInfo
6
+
7
+ // Shaped for probeWithAddon (gpu-probe-addon.ts): the probe only wants each
8
+ // device's name and VRAM total, plus system RAM to catch integrated-GPU lies.
9
+ exports.readSystemStats = function readSystemStats() {
10
+ try {
11
+ GPUInfo ??= require('bare-gpu-info')
12
+ const info = new GPUInfo()
13
+ try {
14
+ const devices = []
15
+ for (const gpu of info) {
16
+ if (typeof gpu.memory !== 'number' || !gpu.name) continue
17
+ devices.push({ name: gpu.name, videoMemoryBytes: gpu.memory })
18
+ }
19
+ if (devices.length === 0) return null
20
+ return { memory: { totalBytes: os.totalmem() }, gpu: { devices } }
21
+ } finally {
22
+ info.destroy()
23
+ }
24
+ } catch {
25
+ return null
26
+ }
27
+ }
@@ -0,0 +1 @@
1
+ export function readSystemStats(): unknown
@@ -24,6 +24,7 @@ export interface HarnessLlmConfig {
24
24
  export interface HarnessChildOptions {
25
25
  runner: StowRunner;
26
26
  skillsDir?: string;
27
+ gpuStats?: unknown;
27
28
  wheelsDir?: string;
28
29
  sdkStorage?: SdkStorage;
29
30
  embedDevice?: EmbedDevice;
@@ -46,6 +47,7 @@ export interface HarnessChildOptions {
46
47
  export declare function harnessChild(entry: string, opts: HarnessChildOptions): ChildSpec<RemoteHarness>;
47
48
  export interface SpawnHarnessOptions {
48
49
  entry: string;
50
+ gpuStats?: unknown;
49
51
  skillsDir?: string;
50
52
  wheelsDir?: string;
51
53
  sdkStorage: SdkStorage;
@@ -5,7 +5,7 @@ import { createEmbedConfig } from "./models/embed-config.js";
5
5
  export function harnessChild(entry, opts) {
6
6
  if (opts.logger)
7
7
  setHarnessLog(true);
8
- const { runner, skillsDir, wheelsDir, sdkStorage, embedDevice, embedGpuLayers, embedBatchSize, imageModel, imageClamps, musicModel, musicClamps, llm, onIpc, logger = null, ...spec } = opts;
8
+ const { runner, skillsDir, gpuStats, wheelsDir, sdkStorage, embedDevice, embedGpuLayers, embedBatchSize, imageModel, imageClamps, musicModel, musicClamps, llm, onIpc, logger = null, ...spec } = opts;
9
9
  const embedConfig = createEmbedConfig({
10
10
  device: embedDevice,
11
11
  gpuLayers: embedGpuLayers,
@@ -24,7 +24,8 @@ export function harnessChild(entry, opts) {
24
24
  llm ? JSON.stringify(llm) : '',
25
25
  musicModel ? JSON.stringify(musicModel) : '',
26
26
  musicClamps ? JSON.stringify(musicClamps) : '',
27
- wheelsDir ?? ''
27
+ wheelsDir ?? '',
28
+ gpuStats === undefined ? '' : JSON.stringify(gpuStats)
28
29
  ],
29
30
  logger,
30
31
  create: (ipc) => {
@@ -55,7 +56,8 @@ export function spawnHarness(opts) {
55
56
  opts.llm ? JSON.stringify(opts.llm) : '',
56
57
  opts.musicModel ? JSON.stringify(opts.musicModel) : '',
57
58
  opts.musicClamps ? JSON.stringify(opts.musicClamps) : '',
58
- opts.wheelsDir ?? ''
59
+ opts.wheelsDir ?? '',
60
+ opts.gpuStats === undefined ? '' : JSON.stringify(opts.gpuStats)
59
61
  ]);
60
62
  sidecar.stdout.on('data', (data) => logger?.info('harness', data.toString().trimEnd()));
61
63
  sidecar.stderr.on('data', (data) => logger?.warn('harness', data.toString().trimEnd()));
@@ -4,8 +4,14 @@ type NodeShapedProcess = typeof process & {
4
4
  binding(name: string): Record<string, unknown>;
5
5
  };
6
6
  export declare function nodeShapedProcess(base: typeof process): NodeShapedProcess;
7
- export declare function pyodideLoadOptions(): {
8
- packageCacheDir: string;
9
- } | undefined;
7
+ declare const silentStdio: {
8
+ stdin: () => null;
9
+ stdout: (_msg: string) => void;
10
+ stderr: (_msg: string) => void;
11
+ };
12
+ export declare function pyodideLoadOptions(): PyodideLoadOptions;
13
+ type PyodideLoadOptions = typeof silentStdio & {
14
+ packageCacheDir?: string;
15
+ };
10
16
  export declare const defaultLoad: LoadPyodideFn;
11
17
  export {};
@@ -74,11 +74,16 @@ async function shapeBareGlobals() {
74
74
  g.performance = { now: () => Date.now() - origin, timeOrigin: origin };
75
75
  }
76
76
  }
77
+ const silentStdio = {
78
+ stdin: () => null,
79
+ stdout: (_msg) => { },
80
+ stderr: (_msg) => { }
81
+ };
77
82
  export function pyodideLoadOptions() {
78
83
  const cacheDir = process.env.QVAC_PYODIDE_CACHE_DIR;
79
84
  if (!cacheDir)
80
- return undefined;
81
- return { packageCacheDir: cacheDir };
85
+ return { ...silentStdio };
86
+ return { ...silentStdio, packageCacheDir: cacheDir };
82
87
  }
83
88
  export const defaultLoad = async () => {
84
89
  if (typeof globalThis.Bare !== 'undefined')
@@ -86,9 +91,7 @@ export const defaultLoad = async () => {
86
91
  const moduleName = 'pyodide';
87
92
  const { loadPyodide } = (await import(__rewriteRelativeImportExtension(moduleName)));
88
93
  const options = pyodideLoadOptions();
89
- if (options) {
94
+ if (options.packageCacheDir)
90
95
  seedPackageCache(options.packageCacheDir);
91
- return (await loadPyodide(options));
92
- }
93
- return (await loadPyodide());
96
+ return (await loadPyodide(options));
94
97
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qvac/harness",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "description": "An AI harness implementing generate(context) via @qvac/sdk. Knows nothing about workbench, hyperdb, or chunks — just inference in, events out.",
6
6
  "exports": {
@@ -3,7 +3,9 @@
3
3
  // [5] imageModel JSON, [6] imageClamps JSON,
4
4
  // [7] '1' to enable harness logging (written to `harness.log`, beside assistant.log),
5
5
  // [8] llm config JSON (llmDevice/llmGpuLayers/contextSize/kvCacheK/kvCacheV),
6
- // [9] musicModel JSON, [10] musicClamps JSON, [11] vendored python wheels dir.
6
+ // [9] musicModel JSON, [10] musicClamps JSON, [11] vendored python wheels dir,
7
+ // [12] host-supplied GPU stats JSON.
8
+ import '../dist-lib/lib/hardware/gpu-probe-entry.js'
7
9
  import FileLog from 'bare-file-logger'
8
10
  import fs from 'fs'
9
11
  import { dirname, join } from 'path'
@@ -104,6 +106,7 @@ export default async function start(ipc, ready) {
104
106
  const llmConfig = Bare.argv[8] ? JSON.parse(Bare.argv[8]) : {}
105
107
  const musicModel = Bare.argv[9] ? JSON.parse(Bare.argv[9]) : undefined
106
108
  const musicClamps = Bare.argv[10] ? JSON.parse(Bare.argv[10]) : undefined
109
+ const gpuStats = Bare.argv[12] ? JSON.parse(Bare.argv[12]) : undefined
107
110
  // Only the host knows where the shipped wheels landed.
108
111
  if (Bare.argv[11]) process.env.QVAC_PYTHON_WHEELS_DIR = Bare.argv[11]
109
112
 
@@ -152,6 +155,7 @@ export default async function start(ipc, ready) {
152
155
  )
153
156
  }
154
157
  harness = new Harness({
158
+ gpuStats,
155
159
  tools,
156
160
  skillsDir,
157
161
  imageModel,
@@ -1,23 +1,7 @@
1
1
  /* global Bare */
2
- // What bare-stow generates as a bundle's `__main__.mjs`, as a plain file.
3
- //
4
- // A stow bundle is two halves: the JS inlined into it, and the native addons
5
- // offloaded beside it, because dlopen cannot read a path inside a bundle. The
6
- // tarball carries dist/prebuilds-manifest.json naming those addons, not the
7
- // addons themselves, so whoever spawns the bundle has to put them back from
8
- // their own node_modules — and then the two halves come from different
9
- // installs. @qvac/core 0.1.2 shipped that way and hung on boot: a bare-pipe one
10
- // patch release ahead of the JS calling it, no error, no exit.
11
- //
12
- // Nothing needs a bundle to spawn this. Run under `bare` from inside
13
- // node_modules, every addon already sits beside the JS that calls it, out of
14
- // one install, so they cannot disagree. The bundle stays published for hosts
15
- // that relocate the code out of node_modules — a packaged desktop or mobile
16
- // app, which stow their own entries anyway — and this is for everyone else.
17
- //
18
- // The protocol is bare-stow's own published export and the host end is
19
- // unchanged: bare-supervisor's sidecar runner wraps this exactly as it wraps
20
- // a bundle.
2
+ // bare-stow's generated bundle shim (lib/shim.js) as a plain file, so the
3
+ // sidecar can be spawned without stowing one.
4
+ import '../dist-lib/lib/hardware/gpu-probe-entry.js'
21
5
  import protocol from 'bare-stow/protocol'
22
6
 
23
7
  const ipc = protocol.attach(Bare.IPC)