@qvac/harness 0.8.8 → 0.8.10

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 (115) hide show
  1. package/CHANGELOG.md +80 -1
  2. package/dist-lib/index.d.ts +1 -1
  3. package/dist-lib/index.js +51 -50
  4. package/dist-lib/lib/audio-transcode-bare.js +2 -2
  5. package/dist-lib/lib/capabilities/completion.d.ts +1 -2
  6. package/dist-lib/lib/capabilities/completion.js +29 -25
  7. package/dist-lib/lib/capabilities/tool-prompt-log.js +3 -3
  8. package/dist-lib/lib/capabilities/voice.js +7 -6
  9. package/dist-lib/lib/connect.js +18 -17
  10. package/dist-lib/lib/errors.d.ts +266 -0
  11. package/dist-lib/lib/errors.js +189 -0
  12. package/dist-lib/lib/helpers/load-stall.d.ts +1 -1
  13. package/dist-lib/lib/helpers/load-stall.js +2 -2
  14. package/dist-lib/lib/helpers/race-abort.d.ts +2 -1
  15. package/dist-lib/lib/helpers/race-abort.js +3 -4
  16. package/dist-lib/lib/hooks/built-in/arg-repair.js +2 -2
  17. package/dist-lib/lib/hooks/pipeline.js +4 -4
  18. package/dist-lib/lib/hooks/run.js +2 -2
  19. package/dist-lib/lib/log.d.ts +26 -2
  20. package/dist-lib/lib/log.js +66 -16
  21. package/dist-lib/lib/models/model-pool.js +11 -6
  22. package/dist-lib/lib/models/model-source.js +3 -2
  23. package/dist-lib/lib/no-redirect-fetch-bare.js +2 -1
  24. package/dist-lib/lib/no-redirect-fetch-impl.js +2 -1
  25. package/dist-lib/lib/office/extract.js +4 -3
  26. package/dist-lib/lib/office/index.js +2 -1
  27. package/dist-lib/lib/resolution-detector.js +3 -3
  28. package/dist-lib/lib/rpc-session.js +2 -1
  29. package/dist-lib/lib/sdk-loader.js +2 -2
  30. package/dist-lib/lib/serve.js +33 -29
  31. package/dist-lib/lib/spawn.d.ts +2 -4
  32. package/dist-lib/lib/spawn.js +14 -7
  33. package/dist-lib/lib/tools/edit-image.js +2 -1
  34. package/dist-lib/lib/tools/exec/adapters/python/attachments.js +8 -8
  35. package/dist-lib/lib/tools/exec/adapters/python/guarded-fetch.js +7 -6
  36. package/dist-lib/lib/tools/exec/adapters/python/interrupt-timer.js +2 -2
  37. package/dist-lib/lib/tools/exec/adapters/python/memfs.js +2 -2
  38. package/dist-lib/lib/tools/exec/adapters/python/pyodide.js +8 -8
  39. package/dist-lib/lib/tools/exec/adapters/python/run-session.js +11 -7
  40. package/dist-lib/lib/tools/exec/adapters/python/wheels.js +5 -5
  41. package/dist-lib/lib/tools/exec/adapters/python/worker/active.js +2 -2
  42. package/dist-lib/lib/tools/exec/adapters/python/worker/client.js +9 -8
  43. package/dist-lib/lib/tools/exec/adapters/python/worker/protocol.d.ts +2 -0
  44. package/dist-lib/lib/tools/exec/adapters/python/worker/serve.js +4 -4
  45. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.d.ts +2 -1
  46. package/dist-lib/lib/tools/exec/adapters/python/worker/spawn.js +2 -1
  47. package/dist-lib/lib/tools/exec/obsidian/note-name.js +2 -1
  48. package/dist-lib/lib/tools/exec/obsidian/safe-path.js +5 -4
  49. package/dist-lib/lib/tools/exec/obsidian/vault-create.js +3 -2
  50. package/dist-lib/lib/tools/exec/obsidian/vault-fs.js +2 -2
  51. package/dist-lib/lib/tools/exec/obsidian/vault-paths.js +3 -2
  52. package/dist-lib/lib/tools/exec/obsidian/vault-read.js +6 -5
  53. package/dist-lib/lib/tools/exec/obsidian/vault-write.js +13 -12
  54. package/dist-lib/lib/tools/exec/runner.js +2 -1
  55. package/dist-lib/lib/tools/files/edit-file.d.ts +3 -0
  56. package/dist-lib/lib/tools/files/edit-file.js +73 -0
  57. package/dist-lib/lib/tools/files/glob.d.ts +3 -0
  58. package/dist-lib/lib/tools/files/glob.js +63 -0
  59. package/dist-lib/lib/tools/files/grep.d.ts +3 -0
  60. package/dist-lib/lib/tools/files/grep.js +114 -0
  61. package/dist-lib/lib/tools/files/index.d.ts +3 -0
  62. package/dist-lib/lib/tools/files/index.js +23 -0
  63. package/dist-lib/lib/tools/files/list-dir.d.ts +5 -0
  64. package/dist-lib/lib/tools/files/list-dir.js +130 -0
  65. package/dist-lib/lib/tools/files/make-dir.d.ts +3 -0
  66. package/dist-lib/lib/tools/files/make-dir.js +47 -0
  67. package/dist-lib/lib/tools/files/read-file.d.ts +3 -0
  68. package/dist-lib/lib/tools/files/read-file.js +103 -0
  69. package/dist-lib/lib/tools/files/transfer.d.ts +4 -0
  70. package/dist-lib/lib/tools/files/transfer.js +82 -0
  71. package/dist-lib/lib/tools/files/trash-file.d.ts +4 -0
  72. package/dist-lib/lib/tools/files/trash-file.js +61 -0
  73. package/dist-lib/lib/tools/files/write-file.d.ts +3 -0
  74. package/dist-lib/lib/tools/files/write-file.js +55 -0
  75. package/dist-lib/lib/tools/fs/document-text.d.ts +2 -0
  76. package/dist-lib/lib/tools/fs/document-text.js +38 -0
  77. package/dist-lib/lib/tools/fs/edit-match.d.ts +11 -0
  78. package/dist-lib/lib/tools/fs/edit-match.js +85 -0
  79. package/dist-lib/lib/tools/fs/remote.d.ts +2 -1
  80. package/dist-lib/lib/tools/fs/remote.js +4 -4
  81. package/dist-lib/lib/tools/fs/scope.d.ts +1 -1
  82. package/dist-lib/lib/tools/fs/scope.js +3 -2
  83. package/dist-lib/lib/tools/fs/targets.d.ts +10 -0
  84. package/dist-lib/lib/tools/fs/targets.js +32 -0
  85. package/dist-lib/lib/tools/fs/tool-path.d.ts +24 -0
  86. package/dist-lib/lib/tools/fs/tool-path.js +58 -0
  87. package/dist-lib/lib/tools/fs/walk.js +3 -3
  88. package/dist-lib/lib/tools/generate-image.d.ts +2 -2
  89. package/dist-lib/lib/tools/generate-image.js +5 -6
  90. package/dist-lib/lib/tools/office.js +2 -1
  91. package/dist-lib/lib/tools/skill.js +2 -1
  92. package/dist-lib/lib/tools/skills/bundled-dir.js +4 -4
  93. package/dist-lib/lib/tools/skills/catalog.js +2 -2
  94. package/dist-lib/lib/tools/skills/credential-health.js +3 -3
  95. package/dist-lib/lib/tools/skills/mention.js +7 -2
  96. package/dist-lib/lib/tools/skills/overlay-crud.js +3 -2
  97. package/dist-lib/lib/tools/translate.d.ts +2 -2
  98. package/dist-lib/lib/tools/translate.js +5 -6
  99. package/dist-lib/lib/tools/weather-lookup/geocode.js +5 -4
  100. package/dist-lib/lib/web-search/engines/bing.js +2 -1
  101. package/dist-lib/lib/web-search/engines/google.js +2 -1
  102. package/dist-lib/lib/web-search/fetch-html.d.ts +5 -5
  103. package/dist-lib/lib/web-search/fetch-html.js +12 -15
  104. package/package.json +67 -25
  105. package/worker/harness-sidecar.mjs +12 -13
  106. package/dist-lib/lib/tools/create-documents.d.ts +0 -3
  107. package/dist-lib/lib/tools/create-documents.js +0 -95
  108. package/dist-lib/lib/tools/edit-documents.d.ts +0 -3
  109. package/dist-lib/lib/tools/edit-documents.js +0 -132
  110. package/dist-lib/lib/tools/file-glob.d.ts +0 -14
  111. package/dist-lib/lib/tools/file-glob.js +0 -118
  112. package/dist-lib/lib/tools/grep.d.ts +0 -5
  113. package/dist-lib/lib/tools/grep.js +0 -184
  114. package/dist-lib/lib/tools/read-files.d.ts +0 -5
  115. package/dist-lib/lib/tools/read-files.js +0 -157
@@ -1,31 +1,81 @@
1
+ import QvacLogger, {} from '@qvac/logging';
2
+ import { ENV_LOG_LEVEL, LEVEL_PRIORITIES } from '@qvac/logging/constants';
1
3
  import process from 'process';
2
- let enabled = process.env.QVAC_HARNESS_LOG === '1';
4
+ const HOST_SCOPE = 'harness-client';
5
+ let sink = null;
6
+ let host = null;
7
+ const logger = new QvacLogger({
8
+ level: 'debug',
9
+ error: writer('error'),
10
+ warn: writer('warn'),
11
+ info: writer('info'),
12
+ debug: writer('debug')
13
+ });
14
+ const fromEnv = envLevel();
15
+ let configured = logger.getLevel();
16
+ if (process.env.QVAC_HARNESS_LOG !== '1') {
17
+ logger.setLevel('off');
18
+ }
19
+ export const log = logger;
3
20
  export function setHarnessLog(on) {
4
- enabled = on;
21
+ logger.setLevel(on ? configured : 'off');
5
22
  }
6
- export function harnessLogEnabled() {
7
- return enabled;
23
+ export function setHarnessLogLevel(level) {
24
+ logger.setLevel(level);
25
+ }
26
+ export function followHostLevel(level) {
27
+ configured = fromEnv ?? (isLogLevel(level) ? level : 'debug');
28
+ logger.setLevel(configured);
29
+ }
30
+ export function logToHost(next) {
31
+ host = next;
32
+ if (next) {
33
+ followHostLevel(next.getLevel?.());
34
+ }
35
+ }
36
+ export function harnessLogLevel() {
37
+ return logger.getLevel();
38
+ }
39
+ export function isLogLevel(value) {
40
+ return typeof value === 'string' && Object.hasOwn(LEVEL_PRIORITIES, value);
41
+ }
42
+ export function logEnabled(level) {
43
+ const current = logger.getLevel();
44
+ return current !== 'off' && LEVEL_PRIORITIES[level] <= LEVEL_PRIORITIES[current];
8
45
  }
9
- let sink = null;
10
46
  export function setHarnessLogSink(next) {
11
47
  sink = next;
12
48
  }
13
49
  export function hlog(message, data) {
14
- if (!enabled)
15
- return;
16
- const tail = data === undefined ? '' : ` ${safeJson(data)}`;
17
- const line = `${message}${tail}`;
18
- if (sink) {
19
- sink(line);
20
- return;
21
- }
22
- process.stdout.write(`${line}\n`);
50
+ log.info(message, data);
23
51
  }
24
52
  export function hdiag(message, data) {
25
53
  if (!sink)
26
54
  return;
27
- const tail = data === undefined ? '' : ` ${safeJson(data)}`;
28
- sink(`${message}${tail}`);
55
+ sink(format(message, data), 'info');
56
+ }
57
+ function writer(level) {
58
+ return (message, data) => {
59
+ const line = format(message, data);
60
+ if (sink) {
61
+ sink(line, level);
62
+ return;
63
+ }
64
+ if (host) {
65
+ const write = host[level] ?? host.info;
66
+ write.call(host, HOST_SCOPE, line);
67
+ return;
68
+ }
69
+ process.stdout.write(`${line}\n`);
70
+ };
71
+ }
72
+ function envLevel() {
73
+ const value = process.env[ENV_LOG_LEVEL] || process.env[`EXPO_PUBLIC_${ENV_LOG_LEVEL}`];
74
+ const level = value?.toLowerCase();
75
+ return isLogLevel(level) ? level : null;
76
+ }
77
+ function format(message, data) {
78
+ return data === undefined ? message : `${message} ${safeJson(data)}`;
29
79
  }
30
80
  function safeJson(data) {
31
81
  try {
@@ -1,4 +1,5 @@
1
1
  import process from 'process';
2
+ import { HarnessError } from '../errors.js';
2
3
  import { transferProgress } from '../model-download-progress.js';
3
4
  import ResourcePuddle from 'resource-puddle';
4
5
  import { ModelHandle } from './model-handle.js';
@@ -6,7 +7,7 @@ import { defaultContextSize } from './context.js';
6
7
  import { customContextLength } from './model-catalog.js';
7
8
  import { adrenoSafeKvCache } from './kv-cache.js';
8
9
  import { DEFAULT_EMBED_BATCH_SIZE } from './embed-config.js';
9
- import { hlog } from '../log.js';
10
+ import { log } from '../log.js';
10
11
  import { RUNAWAY_STOP_SEQUENCES } from '../helpers/control-tokens.js';
11
12
  import { raceTimeout } from '../helpers/race-timeout.js';
12
13
  import { resolveModelSrc } from './model-source.js';
@@ -330,7 +331,7 @@ export function loadClockGuard(model, sdk, { now = () => Date.now(), monoMs = mo
330
331
  const mono = monoMs();
331
332
  if (wall - lastWall < mono - lastMono - jumpMs) {
332
333
  abandoned = true;
333
- fail(new Error(`model load abandoned: system clock moved backward mid-load: ${model}`));
334
+ fail(new HarnessError('MODEL_LOAD_CLOCK_SKEW', [model]));
334
335
  return;
335
336
  }
336
337
  lastWall = wall;
@@ -393,7 +394,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
393
394
  (chooseContext ? await chooseContext({ ...pending, modelConfig: config }, tier) : tier);
394
395
  const modelConfig = { ...config, ctx_size: ctxSize };
395
396
  const { projectionModelSrc: projection, ...loggedConfig } = modelConfig;
396
- hlog('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
397
+ log.debug('llm load config', { model, projection: Boolean(projection), ...loggedConfig });
397
398
  const admission = await beforeLoad?.({ ...pending, modelConfig });
398
399
  const loadStarted = Date.now();
399
400
  const modelId = await loadClockGuard(model, sdk)
@@ -404,7 +405,7 @@ export function createModelPool(loadSdk, notify, { deviceConfig = {}, loads = cr
404
405
  onProgress: (progress) => notify(progressState(model, progress))
405
406
  })))
406
407
  .finally(() => admission?.release());
407
- hlog('llm loaded', { model, loadMs: Date.now() - loadStarted });
408
+ log.info('llm loaded', { model, loadMs: Date.now() - loadStarted });
408
409
  return { model, modelId, ctxSize, bytes: admission?.bytes };
409
410
  };
410
411
  return new ModelPuddle({
@@ -476,7 +477,11 @@ export function createVoicePool(loadSdk, notify, { modelType, addon, modelConfig
476
477
  .race(attempt().catch((err) => {
477
478
  if (!wantsGpu || gpuRefused || loads.draining)
478
479
  throw err;
479
- hlog('voice gpu load refused, retrying on cpu', { model, modelType, error: String(err) });
480
+ log.warn('voice gpu load refused, retrying on cpu', {
481
+ model,
482
+ modelType,
483
+ error: String(err)
484
+ });
480
485
  gpuRefused = true;
481
486
  return attempt();
482
487
  }))
@@ -510,7 +515,7 @@ function unloadOnGc(loadSdk, notify) {
510
515
  await sdk.unloadModel({ modelId });
511
516
  }
512
517
  catch (err) {
513
- hlog('unload failed', { model, err: errorMessage(err) });
518
+ log.warn('unload failed', { model, err: errorMessage(err) });
514
519
  continue;
515
520
  }
516
521
  notify({ status: 'unloaded', model, catalogChanged: true });
@@ -1,3 +1,4 @@
1
+ import { HarnessError } from '../errors.js';
1
2
  import { audioGenModelByName, curatedModelAsset, customModelSrc } from './model-catalog.js';
2
3
  export async function resolveModelSrc(sdk, model, addon = 'llm') {
3
4
  if (/^(\/|~|\.|file:|https?:|registry:|pear:|hyper:)/.test(model)) {
@@ -10,7 +11,7 @@ export async function resolveModelSrc(sdk, model, addon = 'llm') {
10
11
  if (addon === 'audiogen') {
11
12
  const audio = audioGenModelByName(model);
12
13
  if (!audio) {
13
- throw new Error(`audiogen model not found: ${model}`);
14
+ throw new HarnessError('AUDIOGEN_MODEL_NOT_FOUND', [model]);
14
15
  }
15
16
  return audio.src;
16
17
  }
@@ -20,7 +21,7 @@ export async function resolveModelSrc(sdk, model, addon = 'llm') {
20
21
  }
21
22
  const [entry] = await sdk.modelRegistrySearch({ filter: model, addon });
22
23
  if (!entry) {
23
- throw new Error(`model not found in registry: ${model} (addon=${addon})`);
24
+ throw new HarnessError('MODEL_NOT_IN_REGISTRY', [model, addon]);
24
25
  }
25
26
  return { ...entry, src: `registry://${entry.registrySource}/${entry.registryPath}` };
26
27
  }
@@ -1,6 +1,7 @@
1
1
  import http, { Agent as HttpAgent } from 'bare-http1';
2
2
  import https, { Agent as HttpsAgent } from 'bare-https';
3
3
  import * as bareDns from 'bare-dns';
4
+ import { HarnessError } from './errors.js';
4
5
  import { isPrivateOrBlocked } from './tools/private-ip.js';
5
6
  import { makeValidatingLookup } from './validating-lookup.js';
6
7
  import { runNoRedirectFetch } from './no-redirect-fetch-impl.js';
@@ -55,5 +56,5 @@ function readMethod(method) {
55
56
  method === 'OPTIONS') {
56
57
  return method;
57
58
  }
58
- throw new Error(`unsupported HTTP method: ${method}`);
59
+ throw new HarnessError('UNSUPPORTED_HTTP_METHOD', [method]);
59
60
  }
@@ -1,4 +1,5 @@
1
1
  import b4a from 'b4a';
2
+ import { HarnessError } from './errors.js';
2
3
  import { DEFAULT_USER_AGENT } from './user-agent.js';
3
4
  const UA_SNIFFING_HOSTS = new Set(['wttr.in']);
4
5
  export function runNoRedirectFetch(request, url, init = {}) {
@@ -55,7 +56,7 @@ export function runNoRedirectFetch(request, url, init = {}) {
55
56
  req.destroy();
56
57
  return;
57
58
  }
58
- const err = new Error(`response exceeded ${init.maxBytes} bytes`);
59
+ const err = new HarnessError('RESPONSE_TOO_LARGE', [init.maxBytes]);
59
60
  req.destroy(err);
60
61
  fail(err);
61
62
  return;
@@ -1,17 +1,18 @@
1
+ import { HarnessError } from '../errors.js';
1
2
  import { errorMessage } from '../guards.js';
2
3
  const MAX_OFFICE_BYTES = 20 * 1024 * 1024;
3
4
  export function extractWith(bytes, { fileName, signal, extract }) {
4
5
  if (bytes.byteLength > MAX_OFFICE_BYTES) {
5
- throw new Error(`document is too large to extract: ${bytes.byteLength} bytes, limit ${MAX_OFFICE_BYTES}`);
6
+ throw new HarnessError('OFFICE_TOO_LARGE', [bytes.byteLength, MAX_OFFICE_BYTES]);
6
7
  }
7
8
  if (signal?.aborted) {
8
- throw new Error(`${fileName}: extraction aborted`);
9
+ throw new HarnessError('OFFICE_EXTRACTION_ABORTED', [fileName]);
9
10
  }
10
11
  try {
11
12
  return extract(bytes);
12
13
  }
13
14
  catch (err) {
14
- throw new Error(`${fileName}: ${errorMessage(err)}`);
15
+ throw new HarnessError('OFFICE_EXTRACTION_FAILED', [fileName, errorMessage(err)]);
15
16
  }
16
17
  }
17
18
  export function cellText(value) {
@@ -1,4 +1,5 @@
1
1
  import path from 'path';
2
+ import { HarnessError } from '../errors.js';
2
3
  import { extractDocxText } from '../docx/index.js';
3
4
  import { pptx, xlsx } from './engine/index.js';
4
5
  import { cellText, extractWith } from './extract.js';
@@ -89,5 +90,5 @@ export async function extractOfficeText(bytes, fileName, options = {}) {
89
90
  if (extract) {
90
91
  return extractWith(bytes, { fileName, signal: options.signal, extract });
91
92
  }
92
- throw new Error(`Unsupported Office document: ${fileName}`);
93
+ throw new HarnessError('OFFICE_UNSUPPORTED_DOCUMENT', [fileName]);
93
94
  }
@@ -1,5 +1,5 @@
1
1
  import { errorMessage, isRecord } from './guards.js';
2
- import { hlog } from './log.js';
2
+ import { log } from './log.js';
3
3
  function readValue(value, key) {
4
4
  if (value === null)
5
5
  return undefined;
@@ -80,7 +80,7 @@ export function installResolutionDetector(meta) {
80
80
  const wrapped = (specifier, parentURL, opts = {}) => {
81
81
  const parent = String(parentURL);
82
82
  if (isReferrerLost(opts.referrer, parent, cacheRecord)) {
83
- hlog('diag referrer LOST', {
83
+ log.warn('diag referrer LOST', {
84
84
  specifier,
85
85
  parent,
86
86
  modules: Object.keys(cacheRecord).length,
@@ -92,7 +92,7 @@ export function installResolutionDetector(meta) {
92
92
  }
93
93
  catch (err) {
94
94
  if (isReferrerLost(opts.referrer, parent, cacheRecord)) {
95
- hlog('diag resolve FAILED after a lost referrer', resolutionFailure(specifier, parent, err));
95
+ log.error('diag resolve FAILED after a lost referrer', resolutionFailure(specifier, parent, err));
96
96
  }
97
97
  throw err;
98
98
  }
@@ -1,8 +1,9 @@
1
+ import { HarnessError } from './errors.js';
1
2
  export function settleOnClose(hrpc) {
2
3
  const session = hrpc;
3
4
  let reason = null;
4
5
  const dead = (err) => {
5
- reason ??= err ?? new Error('rpc stream destroyed');
6
+ reason ??= err ?? new HarnessError('RPC_STREAM_DESTROYED');
6
7
  };
7
8
  const call = session._call.bind(session);
8
9
  const callSync = session._callSync.bind(session);
@@ -1,6 +1,6 @@
1
1
  import { errorMessage } from './guards.js';
2
2
  import { retryMemo } from './helpers/retry-memo.js';
3
- import { hlog } from './log.js';
3
+ import { log } from './log.js';
4
4
  const defaultImports = {
5
5
  sdk: () => import('#sdk'),
6
6
  plugins: () => import('@qvac/sdk/plugins'),
@@ -14,7 +14,7 @@ function register(registry, name, plugin) {
14
14
  registry.registerPlugin(plugin);
15
15
  }
16
16
  catch (err) {
17
- hlog('plugin registration skipped', {
17
+ log.warn('plugin registration skipped', {
18
18
  plugin: name,
19
19
  err: errorMessage(err)
20
20
  });
@@ -1,6 +1,7 @@
1
1
  import { AbortController } from 'bare-abort-controller';
2
2
  import b4a from 'b4a';
3
3
  import fs from 'fs';
4
+ import { HarnessError } from './errors.js';
4
5
  import { settleOnClose } from './rpc-session.js';
5
6
  import { pipeline, Readable } from 'streamx';
6
7
  import { bindApi } from '../spec/bind.js';
@@ -9,7 +10,7 @@ import { isCapability } from './capabilities/invoke.js';
9
10
  import { CUSTOM_TASK_TYPES } from './models/model-catalog.js';
10
11
  import { unsupportedKvCacheDeletion } from './capabilities/kv-cache.js';
11
12
  import { errorMessage, isRecord } from './guards.js';
12
- import { hlog } from './log.js';
13
+ import { log } from './log.js';
13
14
  import { pushQueue } from './queue.js';
14
15
  import { TextCoalescer } from './helpers/text-coalescer.js';
15
16
  import { prepareOpenclCache } from './models/opencl-cache.js';
@@ -57,7 +58,7 @@ export function serveHarness(stream, harness) {
57
58
  },
58
59
  memoryUsage: async () => {
59
60
  if (!harness.memoryUsage) {
60
- throw new Error('harness does not support memoryUsage');
61
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['memoryUsage']);
61
62
  }
62
63
  return harness.memoryUsage();
63
64
  },
@@ -109,7 +110,7 @@ export function serveHarness(stream, harness) {
109
110
  },
110
111
  previewModelSource: async ({ src, mmprojSrc }) => {
111
112
  if (!harness.previewModelSource) {
112
- throw new Error('harness does not support previewModelSource');
113
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['previewModelSource']);
113
114
  }
114
115
  return harness.previewModelSource({ src: src ?? '', mmprojSrc: mmprojSrc ?? undefined });
115
116
  },
@@ -127,13 +128,14 @@ export function serveHarness(stream, harness) {
127
128
  skills: []
128
129
  },
129
130
  embed: async ({ model, texts }) => {
130
- if (!harness.embed)
131
- throw new Error('harness does not support embed');
131
+ if (!harness.embed) {
132
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['embed']);
133
+ }
132
134
  return packVectors(await harness.embed({ model, texts: texts ?? [] }));
133
135
  },
134
136
  transcribe: async ({ model, audio }) => {
135
137
  if (!harness.transcribe) {
136
- throw new Error('harness does not support transcribe');
138
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['transcribe']);
137
139
  }
138
140
  return {
139
141
  text: await harness.transcribe({ model: model ?? '', audio: audio ?? b4a.alloc(0) })
@@ -141,7 +143,7 @@ export function serveHarness(stream, harness) {
141
143
  },
142
144
  detectLanguage: async ({ text, topK }) => {
143
145
  if (!harness.detectLanguage) {
144
- throw new Error('harness does not support detectLanguage');
146
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['detectLanguage']);
145
147
  }
146
148
  const languages = await harness.detectLanguage({
147
149
  text: text ?? '',
@@ -150,8 +152,9 @@ export function serveHarness(stream, harness) {
150
152
  return { languages };
151
153
  },
152
154
  speak: async ({ model, text }) => {
153
- if (!harness.speak)
154
- throw new Error('harness does not support speak');
155
+ if (!harness.speak) {
156
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['speak']);
157
+ }
155
158
  return harness.speak({ model: model ?? '', text: text ?? '' });
156
159
  },
157
160
  speakStream: speakStream(harness),
@@ -195,26 +198,26 @@ export function serveHarness(stream, harness) {
195
198
  }),
196
199
  capability: async () => {
197
200
  if (!harness.capability) {
198
- throw new Error('harness does not support capability');
201
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['capability']);
199
202
  }
200
203
  return harness.capability();
201
204
  },
202
205
  warmModel: async ({ model, agent }) => {
203
206
  if (!harness.warmModel) {
204
- throw new Error('harness does not support warmModel');
207
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['warmModel']);
205
208
  }
206
209
  await harness.warmModel({ model, agent: agent ?? {} });
207
210
  return { ok: true };
208
211
  },
209
212
  loadModel: async ({ model, agent }) => {
210
213
  if (!harness.loadModel) {
211
- throw new Error('harness does not support loadModel');
214
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['loadModel']);
212
215
  }
213
216
  return harness.loadModel({ model, agent: agent ?? {} });
214
217
  },
215
218
  unloadModel: async ({ model }) => {
216
219
  if (!harness.unloadModel) {
217
- throw new Error('harness does not support unloadModel');
220
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['unloadModel']);
218
221
  }
219
222
  return harness.unloadModel({ model });
220
223
  },
@@ -255,7 +258,7 @@ function toWireSkill(skill) {
255
258
  };
256
259
  }
257
260
  function unsupportedEmbeddingDeviceControl() {
258
- return new Error('harness does not support embedding device control');
261
+ return new HarnessError('CAPABILITY_UNSUPPORTED', ['embedding device control']);
259
262
  }
260
263
  function modelEvents(harness) {
261
264
  let last = '';
@@ -328,7 +331,7 @@ function modelEvents(harness) {
328
331
  function speakStream(harness) {
329
332
  return async function* (duplex) {
330
333
  if (!harness.speakStream) {
331
- throw new Error('harness does not support speakStream');
334
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['speakStream']);
332
335
  }
333
336
  const text = pushQueue();
334
337
  const end = () => text.end();
@@ -349,11 +352,12 @@ function runFrame(harness, frame, context) {
349
352
  if (frame.type !== 'invoke') {
350
353
  return harness.completion(context);
351
354
  }
352
- if (!harness.invoke)
353
- throw new Error('harness does not support invoke');
355
+ if (!harness.invoke) {
356
+ throw new HarnessError('CAPABILITY_UNSUPPORTED', ['invoke']);
357
+ }
354
358
  const capability = frame.capability ?? '';
355
359
  if (!isCapability(capability)) {
356
- throw new Error(`unknown capability: ${capability}`);
360
+ throw new HarnessError('UNKNOWN_CAPABILITY', [capability]);
357
361
  }
358
362
  return harness.invoke({
359
363
  capability,
@@ -386,15 +390,15 @@ function completion(harness) {
386
390
  pending.clear();
387
391
  for (const { reject, staged } of pendingAttach.values()) {
388
392
  void fs.promises.unlink(staged).catch(noop);
389
- reject(new Error('client gone'));
393
+ reject(new HarnessError('CLIENT_GONE'));
390
394
  }
391
395
  pendingAttach.clear();
392
396
  for (const { reject } of pendingFetch.values()) {
393
- reject(new Error('client gone'));
397
+ reject(new HarnessError('CLIENT_GONE'));
394
398
  }
395
399
  pendingFetch.clear();
396
400
  for (const { reject } of pendingRag.values()) {
397
- reject(new Error('client gone'));
401
+ reject(new HarnessError('CLIENT_GONE'));
398
402
  }
399
403
  pendingRag.clear();
400
404
  for (const { resolve } of pendingQuestion.values()) {
@@ -405,7 +409,7 @@ function completion(harness) {
405
409
  if (path) {
406
410
  void fs.promises.unlink(path).catch(noop);
407
411
  }
408
- reject(new Error('client gone'));
412
+ reject(new HarnessError('CLIENT_GONE'));
409
413
  }
410
414
  pendingDrive.clear();
411
415
  events.end();
@@ -498,7 +502,7 @@ function completion(harness) {
498
502
  return;
499
503
  }
500
504
  if (frame.message) {
501
- hlog('question failed', { callId: frame.callId, message: frame.message });
505
+ log.warn('question failed', { callId: frame.callId, message: frame.message });
502
506
  }
503
507
  waiter.resolve(frame.answer || null);
504
508
  return;
@@ -551,7 +555,7 @@ function completion(harness) {
551
555
  : undefined,
552
556
  driveOp: frame.grantedDrives?.length
553
557
  ? (request) => controller.signal.aborted
554
- ? Promise.reject(new Error('client gone'))
558
+ ? Promise.reject(new HarnessError('CLIENT_GONE'))
555
559
  : new Promise((resolve, reject) => {
556
560
  const callId = String(calls++);
557
561
  pendingDrive.set(callId, {
@@ -582,7 +586,7 @@ function completion(harness) {
582
586
  const staged = await stageFile('attach', input.bytes);
583
587
  if (controller.signal.aborted) {
584
588
  void fs.promises.unlink(staged).catch(noop);
585
- throw new Error('client gone');
589
+ throw new HarnessError('CLIENT_GONE');
586
590
  }
587
591
  return new Promise((resolve, reject) => {
588
592
  const callId = String(calls++);
@@ -598,7 +602,7 @@ function completion(harness) {
598
602
  });
599
603
  },
600
604
  fetchAttachment: ({ attachmentId, meta } = {}) => controller.signal.aborted
601
- ? Promise.reject(new Error('client gone'))
605
+ ? Promise.reject(new HarnessError('CLIENT_GONE'))
602
606
  : new Promise((resolve, reject) => {
603
607
  const callId = String(calls++);
604
608
  pendingFetch.set(callId, { resolve, reject });
@@ -610,7 +614,7 @@ function completion(harness) {
610
614
  });
611
615
  }),
612
616
  ragSearch: ({ query, limit }) => controller.signal.aborted
613
- ? Promise.reject(new Error('client gone'))
617
+ ? Promise.reject(new HarnessError('CLIENT_GONE'))
614
618
  : new Promise((resolve, reject) => {
615
619
  const callId = String(calls++);
616
620
  pendingRag.set(callId, { resolve, reject });
@@ -660,7 +664,7 @@ function completion(harness) {
660
664
  events.push({ type: 'error', message: errorMessage(err) });
661
665
  }
662
666
  events.push({ type: 'done' });
663
- hlog('wire end', { produced, frames });
667
+ log.debug('wire end', { produced, frames });
664
668
  events.end();
665
669
  })();
666
670
  });
@@ -668,7 +672,7 @@ function completion(harness) {
668
672
  yield event;
669
673
  await tick();
670
674
  }
671
- hlog('wire handed off');
675
+ log.info('wire handed off');
672
676
  };
673
677
  }
674
678
  function downloadStream(harness, req) {
@@ -2,6 +2,7 @@ import type { SdkStorage } from './storage.ts';
2
2
  import { type StowIpc, type StowRunner } from 'bare-supervisor/stow';
3
3
  import type { ChildSpec } from 'bare-supervisor';
4
4
  import { type RemoteHarness } from './connect.ts';
5
+ import { type HostLogger } from './log.ts';
5
6
  import { type EmbedDevice } from './models/embed-config.ts';
6
7
  import type { ImageClamps } from './models/model-catalog.ts';
7
8
  import type { ImageModelSet } from './tools/generate-image.ts';
@@ -10,10 +11,7 @@ export type MusicModelArg = string | (MusicModelStages & {
10
11
  inferenceSteps?: number;
11
12
  useGpu?: boolean;
12
13
  });
13
- export interface Logger {
14
- info(...args: unknown[]): void;
15
- warn(...args: unknown[]): void;
16
- }
14
+ export type Logger = HostLogger;
17
15
  export interface HarnessLlmConfig {
18
16
  llmDevice?: 'cpu' | 'gpu';
19
17
  llmGpuLayers?: number;
@@ -1,10 +1,11 @@
1
+ import { HarnessError } from './errors.js';
1
2
  import stowChild, {} from 'bare-supervisor/stow';
2
3
  import { connectHarness } from './connect.js';
3
- import { setHarnessLog } from './log.js';
4
+ import { isLogLevel, logToHost } from './log.js';
4
5
  import { createEmbedConfig } from './models/embed-config.js';
5
6
  export function harnessChild(entry, opts) {
6
7
  if (opts.logger) {
7
- setHarnessLog(true);
8
+ logToHost(opts.logger);
8
9
  }
9
10
  const { runner, skillsDir, gpuStats, wheelsDir, sdkStorage, embedDevice, embedGpuLayers, embedBatchSize, imageModel, imageClamps, musicModel, musicClamps, llm, onIpc, logger = null, ...spec } = opts;
10
11
  const embedConfig = createEmbedConfig({
@@ -21,7 +22,7 @@ export function harnessChild(entry, opts) {
21
22
  JSON.stringify(embedConfig),
22
23
  imageModel ? JSON.stringify(imageModel) : '',
23
24
  imageClamps ? JSON.stringify(imageClamps) : '',
24
- logger ? '1' : '',
25
+ logSlot(logger),
25
26
  llm ? JSON.stringify(llm) : '',
26
27
  musicModel ? JSON.stringify(musicModel) : '',
27
28
  musicClamps ? JSON.stringify(musicClamps) : '',
@@ -38,7 +39,7 @@ export function harnessChild(entry, opts) {
38
39
  export function spawnHarness(opts) {
39
40
  const logger = opts.logger ?? null;
40
41
  if (logger) {
41
- setHarnessLog(true);
42
+ logToHost(logger);
42
43
  }
43
44
  const embedConfig = createEmbedConfig({
44
45
  device: opts.embedDevice,
@@ -53,18 +54,24 @@ export function spawnHarness(opts) {
53
54
  JSON.stringify(embedConfig),
54
55
  opts.imageModel ? JSON.stringify(opts.imageModel) : '',
55
56
  opts.imageClamps ? JSON.stringify(opts.imageClamps) : '',
56
- logger ? '1' : '',
57
+ logSlot(logger),
57
58
  opts.llm ? JSON.stringify(opts.llm) : '',
58
59
  opts.musicModel ? JSON.stringify(opts.musicModel) : '',
59
60
  opts.musicClamps ? JSON.stringify(opts.musicClamps) : '',
60
61
  opts.wheelsDir ?? '',
61
62
  opts.gpuStats === undefined ? '' : JSON.stringify(opts.gpuStats)
62
63
  ], logger);
63
- const died = exit.then((code) => `harness exited (${code}) before ready`);
64
+ const died = exit.then((code) => new HarnessError('HARNESS_EXITED_BEFORE_READY', [code]));
64
65
  const failure = await Promise.race([ipc.ready.then(() => null), died]);
65
66
  if (failure) {
66
- throw new Error(failure);
67
+ throw failure;
67
68
  }
68
69
  return ipc;
69
70
  });
70
71
  }
72
+ function logSlot(logger) {
73
+ if (!logger)
74
+ return '';
75
+ const level = logger.getLevel?.();
76
+ return isLogLevel(level) ? level : '1';
77
+ }
@@ -1,6 +1,7 @@
1
1
  import b4a from 'b4a';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import { isHarnessError } from '../errors.js';
4
5
  import { AbortedError } from '../helpers/race-abort.js';
5
6
  import { BUSY_ERROR, DIM_STEP, effectiveSteps, generationPercentTick, IMAGE_LOAD_STALL_ERROR, normalizeClamps, parseCommonArgs, runDiffusion } from './generate-image.js';
6
7
  const DEFAULT_STRENGTH = 0.7;
@@ -278,7 +279,7 @@ export function createEditImageTool(runtime, { loadMedia = defaultLoadMedia, res
278
279
  if (err instanceof AbortedError) {
279
280
  return { error: 'image generation canceled' };
280
281
  }
281
- if (err instanceof Error && err.message === IMAGE_LOAD_STALL_ERROR) {
282
+ if (isHarnessError(err, 'IMAGE_LOAD_STALLED')) {
282
283
  return { error: IMAGE_LOAD_STALL_ERROR };
283
284
  }
284
285
  throw err;