@qvac/harness 0.2.3 → 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.
- package/dist/harness.bundle +78 -15
- package/dist/prebuilds-manifest.json +8 -0
- package/dist-lib/index.d.ts +3 -1
- package/dist-lib/index.js +5 -3
- package/dist-lib/lib/hardware/capability-probe.d.ts +1 -1
- package/dist-lib/lib/hardware/capability-probe.js +1 -1
- package/dist-lib/lib/hardware/gpu-probe-entry.d.ts +1 -0
- package/dist-lib/lib/hardware/gpu-probe-entry.js +2 -0
- package/dist-lib/lib/hardware/gpu-probe.d.ts +8 -0
- package/dist-lib/lib/hardware/gpu-probe.js +54 -0
- package/dist-lib/lib/hardware/gpu-stats.bare.d.ts +1 -1
- package/dist-lib/lib/hardware/gpu-stats.bare.js +1 -24
- package/dist-lib/lib/hardware/gpu-stats.native.cjs +27 -0
- package/dist-lib/lib/hardware/gpu-stats.native.d.cts +1 -0
- package/dist-lib/lib/spawn.d.ts +2 -0
- package/dist-lib/lib/spawn.js +5 -3
- package/dist-lib/lib/tools/exec/adapters/python/loader.d.ts +9 -3
- package/dist-lib/lib/tools/exec/adapters/python/loader.js +9 -6
- package/package.json +2 -1
- package/worker/harness-sidecar.mjs +178 -0
- package/worker/stow-entry.mjs +52 -0
|
@@ -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",
|
package/dist-lib/index.d.ts
CHANGED
|
@@ -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 {
|
|
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: () =>
|
|
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,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
|
|
1
|
+
export { readSystemStats } from './gpu-stats.native.cjs';
|
|
@@ -1,24 +1 @@
|
|
|
1
|
-
|
|
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
|
package/dist-lib/lib/spawn.d.ts
CHANGED
|
@@ -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;
|
package/dist-lib/lib/spawn.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": {
|
|
@@ -175,6 +175,7 @@
|
|
|
175
175
|
"default": "./dist-lib/schema/release-tags.js"
|
|
176
176
|
},
|
|
177
177
|
"./dist/harness.cjs": "./dist/harness.cjs",
|
|
178
|
+
"./worker/stow-entry.mjs": "./worker/stow-entry.mjs",
|
|
178
179
|
"./lib/tools/catalog.ts": {
|
|
179
180
|
"types": "./dist-lib/lib/tools/catalog.d.ts",
|
|
180
181
|
"default": "./dist-lib/lib/tools/catalog.js"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// bare-stow entry: serves the Harness over the ipc duplex bare-stow wires.
|
|
2
|
+
// argv (empty = absent): [2] skillsDir, [3] SDK storage JSON (required: the fs deny list fences it), [4] embedding config JSON,
|
|
3
|
+
// [5] imageModel JSON, [6] imageClamps JSON,
|
|
4
|
+
// [7] '1' to enable harness logging (written to `harness.log`, beside assistant.log),
|
|
5
|
+
// [8] llm config JSON (llmDevice/llmGpuLayers/contextSize/kvCacheK/kvCacheV),
|
|
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'
|
|
9
|
+
import FileLog from 'bare-file-logger'
|
|
10
|
+
import fs from 'fs'
|
|
11
|
+
import { dirname, join } from 'path'
|
|
12
|
+
import process from 'bare-process'
|
|
13
|
+
import { configureSdkStorage } from '../dist-lib/lib/sdk-storage.js'
|
|
14
|
+
import {
|
|
15
|
+
Harness,
|
|
16
|
+
loadSkillCatalog,
|
|
17
|
+
serveHarness,
|
|
18
|
+
setHarnessLog,
|
|
19
|
+
setHarnessLogSink
|
|
20
|
+
} from '../dist-lib/index.js'
|
|
21
|
+
import { createCreateDocumentsTool } from '../dist-lib/lib/tools/create-documents.js'
|
|
22
|
+
import { createEditDocumentsTool } from '../dist-lib/lib/tools/edit-documents.js'
|
|
23
|
+
import { createExecTool } from '../dist-lib/lib/tools/exec.js'
|
|
24
|
+
import { createFileGlobTool } from '../dist-lib/lib/tools/file-glob.js'
|
|
25
|
+
import { deniedRootsFor } from '../dist-lib/lib/tools/fs/scope.js'
|
|
26
|
+
import { createGrepTool } from '../dist-lib/lib/tools/grep.js'
|
|
27
|
+
import { createHttpRequestTool } from '../dist-lib/lib/tools/http-request.js'
|
|
28
|
+
import { createMcpCallTool } from '../dist-lib/lib/tools/mcp-call.js'
|
|
29
|
+
import { compileOperationTools } from '../dist-lib/lib/tools/operations/compile.js'
|
|
30
|
+
import { createReadFilesTool } from '../dist-lib/lib/tools/read-files.js'
|
|
31
|
+
import { createSkillTool } from '../dist-lib/lib/tools/skill.js'
|
|
32
|
+
import { ensureBundledSkillsDir } from '../dist-lib/lib/tools/skills/bundled-dir.js'
|
|
33
|
+
import { credentialAllowListFor, mcpReadMethods } from '../dist-lib/lib/tools/skills/index.js'
|
|
34
|
+
import { createWebFetchTool } from '../dist-lib/lib/tools/web-fetch.js'
|
|
35
|
+
import { createWebSearchTool } from '../dist-lib/lib/tools/web-search.js'
|
|
36
|
+
|
|
37
|
+
// at maxSize rotate to a single overwritten archive, as the assistant's Logger does
|
|
38
|
+
const LOG_MAX_SIZE = 100_000_000
|
|
39
|
+
const SCOPE = '[harness]'
|
|
40
|
+
// when the file will not open, everything still has somewhere to go that is not the pipe
|
|
41
|
+
const DISCARD = { info() {}, debug() {}, warn() {}, error() {} }
|
|
42
|
+
|
|
43
|
+
// On Windows the host does not read our stdout, and writing past the OS buffer takes the
|
|
44
|
+
// Bare thread with it: the event loop stops, nothing raises, and a download never produces
|
|
45
|
+
// a byte. Elsewhere the same write only queues in memory (measured), which is why it only
|
|
46
|
+
// showed up there. Every line goes to a file instead, via the FileLog the assistant uses.
|
|
47
|
+
function openHarnessLog(path) {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(dirname(path), { recursive: true })
|
|
50
|
+
return new FileLog(path, { maxSize: LOG_MAX_SIZE, rotate: (p) => `${p}.1` })
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Ungated by the host's log flag: the SDK and its addons log through `console`, and a
|
|
57
|
+
// chatty dependency must not be able to wedge this process.
|
|
58
|
+
function captureConsole(log) {
|
|
59
|
+
console.log = console.info = (...args) => log.info(SCOPE, ...args)
|
|
60
|
+
console.debug = (...args) => log.debug(SCOPE, ...args)
|
|
61
|
+
console.warn = (...args) => log.warn(SCOPE, ...args)
|
|
62
|
+
console.error = (...args) => log.error(SCOPE, ...args)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The SDK's logger has console on by default, whoever asked; follow the host instead.
|
|
66
|
+
async function alignSdkConsoleWithHost(hostWantsLogs) {
|
|
67
|
+
try {
|
|
68
|
+
const logging = await import('@qvac/sdk/logging')
|
|
69
|
+
logging.setGlobalConsoleOutput(hostWantsLogs)
|
|
70
|
+
} catch {
|
|
71
|
+
// a log setting must not decide whether this process boots
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default async function start(ipc, ready) {
|
|
76
|
+
let harness
|
|
77
|
+
let dying = false
|
|
78
|
+
// close() cancels in-flight SDK model loads (see Harness._close) so exit is not
|
|
79
|
+
// parked behind a multi-GB load; a wedged native call still ends at the host's
|
|
80
|
+
// kill deadline (bare-supervisor/stow reap)
|
|
81
|
+
const die = () => {
|
|
82
|
+
if (dying) return
|
|
83
|
+
dying = true
|
|
84
|
+
Promise.resolve(harness?.close())
|
|
85
|
+
.catch(() => {})
|
|
86
|
+
.then(() => Bare.exit(0))
|
|
87
|
+
}
|
|
88
|
+
Bare.IPC.on('end', die).on('close', die)
|
|
89
|
+
|
|
90
|
+
const sdkStorage = JSON.parse(Bare.argv[3] || '{}')
|
|
91
|
+
const deniedRoots = deniedRootsFor(sdkStorage.home)
|
|
92
|
+
const hostWantsLogs = Bare.argv[7] === '1'
|
|
93
|
+
setHarnessLog(hostWantsLogs)
|
|
94
|
+
// before the first await, so nothing logs ahead of the sink. The SDK home sits in
|
|
95
|
+
// the storage root, which is where the host writes assistant.log — harness.log
|
|
96
|
+
// belongs next to it.
|
|
97
|
+
const log = openHarnessLog(join(dirname(sdkStorage.home), 'harness.log')) ?? DISCARD
|
|
98
|
+
setHarnessLogSink((line) => log.info(SCOPE, line))
|
|
99
|
+
captureConsole(log)
|
|
100
|
+
await alignSdkConsoleWithHost(hostWantsLogs)
|
|
101
|
+
|
|
102
|
+
const skillsDir = Bare.argv[2] || (await ensureBundledSkillsDir())
|
|
103
|
+
const embedConfig = JSON.parse(Bare.argv[4] || '{}')
|
|
104
|
+
const imageModel = Bare.argv[5] ? JSON.parse(Bare.argv[5]) : undefined
|
|
105
|
+
const imageClamps = Bare.argv[6] ? JSON.parse(Bare.argv[6]) : undefined
|
|
106
|
+
const llmConfig = Bare.argv[8] ? JSON.parse(Bare.argv[8]) : {}
|
|
107
|
+
const musicModel = Bare.argv[9] ? JSON.parse(Bare.argv[9]) : undefined
|
|
108
|
+
const musicClamps = Bare.argv[10] ? JSON.parse(Bare.argv[10]) : undefined
|
|
109
|
+
const gpuStats = Bare.argv[12] ? JSON.parse(Bare.argv[12]) : undefined
|
|
110
|
+
// Only the host knows where the shipped wheels landed.
|
|
111
|
+
if (Bare.argv[11]) process.env.QVAC_PYTHON_WHEELS_DIR = Bare.argv[11]
|
|
112
|
+
|
|
113
|
+
configureSdkStorage(sdkStorage)
|
|
114
|
+
process.env.QVAC_PYODIDE_CACHE_DIR = `${sdkStorage.home}/pyodide-cache`
|
|
115
|
+
|
|
116
|
+
const skillCatalog = skillsDir ? await loadSkillCatalog(skillsDir) : undefined
|
|
117
|
+
const credentialed = skillCatalog
|
|
118
|
+
? {
|
|
119
|
+
credentialStore: (key, ctx) => harness?.resolveCredential(key, ctx) ?? null,
|
|
120
|
+
credentialAllowList: credentialAllowListFor(skillCatalog)
|
|
121
|
+
}
|
|
122
|
+
: undefined
|
|
123
|
+
|
|
124
|
+
const httpRequestTool = createHttpRequestTool(credentialed)
|
|
125
|
+
const mcpCallTool = createMcpCallTool({
|
|
126
|
+
...credentialed,
|
|
127
|
+
readMethods: skillCatalog ? mcpReadMethods(skillCatalog) : undefined
|
|
128
|
+
})
|
|
129
|
+
const tools = [
|
|
130
|
+
httpRequestTool,
|
|
131
|
+
createExecTool({
|
|
132
|
+
...credentialed,
|
|
133
|
+
deniedRoots,
|
|
134
|
+
skillsDir,
|
|
135
|
+
skillSettings: (skill, agentId) => harness?.skillSettings(skill, agentId) ?? {}
|
|
136
|
+
}),
|
|
137
|
+
mcpCallTool,
|
|
138
|
+
createWebSearchTool(),
|
|
139
|
+
createWebFetchTool(),
|
|
140
|
+
createReadFilesTool({ deniedRoots }),
|
|
141
|
+
createFileGlobTool({ deniedRoots }),
|
|
142
|
+
createGrepTool({ deniedRoots }),
|
|
143
|
+
createCreateDocumentsTool({ deniedRoots }),
|
|
144
|
+
createEditDocumentsTool({ deniedRoots })
|
|
145
|
+
]
|
|
146
|
+
if (skillCatalog) {
|
|
147
|
+
tools.push(...compileOperationTools(skillCatalog, { httpRequestTool, mcpCallTool }))
|
|
148
|
+
}
|
|
149
|
+
if (skillsDir) {
|
|
150
|
+
tools.push(
|
|
151
|
+
createSkillTool({
|
|
152
|
+
skillsDir,
|
|
153
|
+
...(credentialed ? { credentialStore: credentialed.credentialStore } : {})
|
|
154
|
+
})
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
harness = new Harness({
|
|
158
|
+
gpuStats,
|
|
159
|
+
tools,
|
|
160
|
+
skillsDir,
|
|
161
|
+
imageModel,
|
|
162
|
+
imageClamps,
|
|
163
|
+
musicModel,
|
|
164
|
+
musicClamps,
|
|
165
|
+
embedDevice: embedConfig.device,
|
|
166
|
+
embedGpuLayers: embedConfig.gpuLayers,
|
|
167
|
+
embedBatchSize: embedConfig.batchSize,
|
|
168
|
+
llmDevice: llmConfig.llmDevice,
|
|
169
|
+
llmGpuLayers: llmConfig.llmGpuLayers,
|
|
170
|
+
contextSize: llmConfig.contextSize,
|
|
171
|
+
kvCacheK: llmConfig.kvCacheK,
|
|
172
|
+
kvCacheV: llmConfig.kvCacheV
|
|
173
|
+
})
|
|
174
|
+
serveHarness(ipc, harness)
|
|
175
|
+
ready()
|
|
176
|
+
|
|
177
|
+
return () => {}
|
|
178
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/* global Bare */
|
|
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'
|
|
5
|
+
import protocol from 'bare-stow/protocol'
|
|
6
|
+
|
|
7
|
+
const ipc = protocol.attach(Bare.IPC)
|
|
8
|
+
|
|
9
|
+
let stop = null
|
|
10
|
+
let readied = false
|
|
11
|
+
let exited = false
|
|
12
|
+
|
|
13
|
+
function ready() {
|
|
14
|
+
if (readied) return
|
|
15
|
+
readied = true
|
|
16
|
+
|
|
17
|
+
ipc.send('ready')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function shutdown(code, err = null) {
|
|
21
|
+
if (exited) return
|
|
22
|
+
exited = true
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
if (stop) await stop()
|
|
26
|
+
} catch (e) {
|
|
27
|
+
err = err || e
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (err) ipc.send('error', { message: err.message, stack: err.stack })
|
|
31
|
+
|
|
32
|
+
await ipc.send('exit', { code })
|
|
33
|
+
|
|
34
|
+
Bare.exit(code)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ipc.on('terminate', () => shutdown(0))
|
|
38
|
+
|
|
39
|
+
Bare.on('beforeExit', (exitCode) => shutdown(exitCode))
|
|
40
|
+
.on('uncaughtException', (err) => shutdown(1, err))
|
|
41
|
+
.on('unhandledRejection', (err) => shutdown(1, err))
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const entry = await import('./harness-sidecar.mjs')
|
|
45
|
+
const start = entry.default
|
|
46
|
+
|
|
47
|
+
stop = (await start(ipc, ready)) || null
|
|
48
|
+
|
|
49
|
+
ready()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
shutdown(1, err)
|
|
52
|
+
}
|