@qvac/sdk 0.13.3 → 0.13.4
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/client/rpc/node-rpc-client.js +114 -45
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -2
- package/dist/schemas/sdk-errors-client.d.ts +1 -0
- package/dist/schemas/sdk-errors-client.js +5 -0
- package/dist/server/utils/tools/parsers/hermes.js +24 -2
- package/dist/utils/errors-client.d.ts +3 -0
- package/dist/utils/errors-client.js +5 -0
- package/package.json +1 -1
|
@@ -10,8 +10,9 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { initializeConfig } from "../init-hooks.js";
|
|
11
11
|
import { resolveConfig } from "../config-loader/resolve-config.node.js";
|
|
12
12
|
import { getClientLogger } from "../../logging/index.js";
|
|
13
|
-
import { RPCInitTimeoutError, WorkerCrashedError, WorkerShutdownError, } from "../../utils/errors-client.js";
|
|
13
|
+
import { BareRuntimeBinaryNotFoundError, RPCInitTimeoutError, WorkerCrashedError, WorkerShutdownError, } from "../../utils/errors-client.js";
|
|
14
14
|
const RPC_INIT_TIMEOUT_MS = 30_000;
|
|
15
|
+
const WORKER_STDERR_TAIL_CHARS = 16_384;
|
|
15
16
|
const logger = getClientLogger();
|
|
16
17
|
let rpcInstance = null;
|
|
17
18
|
let rpcPromise = null;
|
|
@@ -144,21 +145,74 @@ function bestEffortUnlinkSocket(socketPath) {
|
|
|
144
145
|
logger.debug("Failed to unlink IPC socket path", { socketPath, error });
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
function
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
function appendWorkerStderrTail(current, chunk) {
|
|
149
|
+
const next = current + chunk;
|
|
150
|
+
if (next.length <= WORKER_STDERR_TAIL_CHARS)
|
|
151
|
+
return next;
|
|
152
|
+
return next.slice(next.length - WORKER_STDERR_TAIL_CHARS);
|
|
153
|
+
}
|
|
154
|
+
function createWorkerStartupError(details, stderrTail) {
|
|
155
|
+
const stderr = stderrTail.trimEnd();
|
|
156
|
+
if (!stderr)
|
|
157
|
+
return new Error(details);
|
|
158
|
+
return new Error(`${details}\n\nWorker stderr:\n${stderr}`);
|
|
159
|
+
}
|
|
160
|
+
function resetModuleState() {
|
|
151
161
|
rpcInstance = null;
|
|
152
162
|
rpcPromise = null;
|
|
153
163
|
bareWorkerProc = null;
|
|
154
164
|
ipcServer = null;
|
|
155
165
|
currentSocketPath = null;
|
|
156
166
|
workerLifeController = null;
|
|
167
|
+
}
|
|
168
|
+
function snapshotAndResetState() {
|
|
169
|
+
const workerToClose = bareWorkerProc;
|
|
170
|
+
const serverToClose = ipcServer;
|
|
171
|
+
const socketPathToClose = currentSocketPath;
|
|
172
|
+
resetModuleState();
|
|
157
173
|
return { workerToClose, serverToClose, socketPathToClose };
|
|
158
174
|
}
|
|
175
|
+
// Shared cleanup for every init reject path: reset module state, stop the
|
|
176
|
+
// worker + IPC server, and remove the socket so a failed start never leaks
|
|
177
|
+
// resources. Mirrors closeSyncForExit without the planned-shutdown abort.
|
|
178
|
+
function teardownFailedInit() {
|
|
179
|
+
const { workerToClose, serverToClose, socketPathToClose } = snapshotAndResetState();
|
|
180
|
+
if (workerToClose) {
|
|
181
|
+
try {
|
|
182
|
+
workerToClose.kill("SIGTERM");
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
logger.debug("Failed to kill bare worker after init failure", { error });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (serverToClose) {
|
|
189
|
+
try {
|
|
190
|
+
serverToClose.close();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger.debug("Failed to close IPC server after init failure", { error });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
bestEffortUnlinkSocket(socketPathToClose);
|
|
197
|
+
}
|
|
159
198
|
export function getWorkerLifeSignal() {
|
|
160
199
|
return workerLifeController?.signal ?? null;
|
|
161
200
|
}
|
|
201
|
+
function getWorkerStderr(proc) {
|
|
202
|
+
return proc.stderr ?? null;
|
|
203
|
+
}
|
|
204
|
+
// `bare-runtime` resolves its platform binary with
|
|
205
|
+
// `require('bare-runtime-<platform>-<arch>')` and throws a terse
|
|
206
|
+
// `No binaries found for target '<platform>-<arch>'` whenever that package —
|
|
207
|
+
// or one of its nested deps — is absent. Under pnpm that happens even on a
|
|
208
|
+
// supported platform, so surface an actionable error instead of the raw throw.
|
|
209
|
+
function mapBareSpawnError(error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
if (/No binar(?:y|ies) found for target/i.test(message)) {
|
|
212
|
+
return new BareRuntimeBinaryNotFoundError(process.platform, process.arch, error);
|
|
213
|
+
}
|
|
214
|
+
return error instanceof Error ? error : new Error(message);
|
|
215
|
+
}
|
|
162
216
|
// `spawn` carries this listener's own captured resources. Module-level
|
|
163
217
|
// state may belong to a newer ensureRPC() by the time we fire.
|
|
164
218
|
function handlePostHandshakeExit(code, exitSignal, spawn) {
|
|
@@ -170,12 +224,7 @@ function handlePostHandshakeExit(code, exitSignal, spawn) {
|
|
|
170
224
|
logger.info(`🪦 Bare worker exited post-handshake (code=${code}, signal=${exitSignal})`);
|
|
171
225
|
// Only clear module state if we're still the active spawn.
|
|
172
226
|
if (workerLifeController === spawn.controller) {
|
|
173
|
-
|
|
174
|
-
rpcPromise = null;
|
|
175
|
-
bareWorkerProc = null;
|
|
176
|
-
ipcServer = null;
|
|
177
|
-
currentSocketPath = null;
|
|
178
|
-
workerLifeController = null;
|
|
227
|
+
resetModuleState();
|
|
179
228
|
}
|
|
180
229
|
try {
|
|
181
230
|
spawn.server.close();
|
|
@@ -223,17 +272,16 @@ async function ensureRPC() {
|
|
|
223
272
|
workerLifeController = spawnController;
|
|
224
273
|
rpcPromise = new Promise((resolve, reject) => {
|
|
225
274
|
let settled = false;
|
|
275
|
+
let workerStderrTail = "";
|
|
226
276
|
const timer = setTimeout(() => {
|
|
227
277
|
if (settled)
|
|
228
278
|
return;
|
|
229
279
|
settled = true;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
workerLifeController = null;
|
|
236
|
-
reject(new RPCInitTimeoutError(RPC_INIT_TIMEOUT_MS));
|
|
280
|
+
const cause = workerStderrTail
|
|
281
|
+
? createWorkerStartupError("Worker did not establish IPC before the RPC initialization timeout", workerStderrTail)
|
|
282
|
+
: undefined;
|
|
283
|
+
teardownFailedInit();
|
|
284
|
+
reject(new RPCInitTimeoutError(RPC_INIT_TIMEOUT_MS, cause));
|
|
237
285
|
}, RPC_INIT_TIMEOUT_MS);
|
|
238
286
|
ipcServer = createServer((socket) => {
|
|
239
287
|
if (settled)
|
|
@@ -248,12 +296,7 @@ async function ensureRPC() {
|
|
|
248
296
|
return;
|
|
249
297
|
settled = true;
|
|
250
298
|
clearTimeout(timer);
|
|
251
|
-
|
|
252
|
-
rpcInstance = null;
|
|
253
|
-
bareWorkerProc = null;
|
|
254
|
-
ipcServer = null;
|
|
255
|
-
currentSocketPath = null;
|
|
256
|
-
workerLifeController = null;
|
|
299
|
+
teardownFailedInit();
|
|
257
300
|
reject(error);
|
|
258
301
|
});
|
|
259
302
|
ipcServer.listen(socketPath, () => {
|
|
@@ -262,35 +305,61 @@ async function ensureRPC() {
|
|
|
262
305
|
server: ipcServer,
|
|
263
306
|
socketPath,
|
|
264
307
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
try {
|
|
309
|
+
bareWorkerProc = spawn("bare", {
|
|
310
|
+
args: [
|
|
311
|
+
WORKER_PATH,
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
QVAC_IPC_SOCKET_PATH: socketPath,
|
|
314
|
+
// Snap's HOME can be revision-scoped; SNAP_USER_COMMON is stable.
|
|
315
|
+
HOME_DIR: process.env["SNAP_USER_COMMON"]
|
|
316
|
+
? String(process.env["SNAP_USER_COMMON"])
|
|
317
|
+
: os.homedir(),
|
|
318
|
+
}),
|
|
319
|
+
],
|
|
320
|
+
platform: process.platform,
|
|
321
|
+
arch: process.arch,
|
|
322
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// `spawn` resolves the bare binary synchronously and can throw before
|
|
327
|
+
// the worker exists, so there is no process to emit "exit". Without
|
|
328
|
+
// this, the throw escapes the `listen` callback as an uncaught
|
|
329
|
+
// exception and crashes the host process.
|
|
330
|
+
if (settled)
|
|
331
|
+
return;
|
|
332
|
+
settled = true;
|
|
333
|
+
clearTimeout(timer);
|
|
334
|
+
teardownFailedInit();
|
|
335
|
+
reject(mapBareSpawnError(error));
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
278
338
|
if (bareWorkerProc) {
|
|
339
|
+
getWorkerStderr(bareWorkerProc)?.on("data", (chunk) => {
|
|
340
|
+
const text = chunk.toString();
|
|
341
|
+
workerStderrTail = appendWorkerStderrTail(workerStderrTail, text);
|
|
342
|
+
process.stderr.write(chunk);
|
|
343
|
+
});
|
|
279
344
|
bareWorkerProc.on("exit", (code, exitSignal) => {
|
|
280
345
|
if (settled) {
|
|
281
346
|
handlePostHandshakeExit(code, exitSignal, spawnResources);
|
|
282
347
|
return;
|
|
283
348
|
}
|
|
284
|
-
//
|
|
349
|
+
// Pre-handshake failures are rejected from "close" so stderr has
|
|
350
|
+
// drained before we assemble the startup error cause.
|
|
351
|
+
});
|
|
352
|
+
bareWorkerProc.on("close", (...args) => {
|
|
353
|
+
if (settled)
|
|
354
|
+
return;
|
|
355
|
+
const code = typeof args[0] === "number" ? args[0] : null;
|
|
356
|
+
const exitSignal = typeof args[1] === "string" ? args[1] : null;
|
|
357
|
+
// Worker died before handshake. Use close, not exit, so piped
|
|
358
|
+
// stderr has drained before we build the error cause.
|
|
285
359
|
settled = true;
|
|
286
360
|
clearTimeout(timer);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
bareWorkerProc = null;
|
|
290
|
-
ipcServer = null;
|
|
291
|
-
currentSocketPath = null;
|
|
292
|
-
workerLifeController = null;
|
|
293
|
-
reject(new RPCInitTimeoutError(RPC_INIT_TIMEOUT_MS, new Error(`Worker process exited with code ${code} before IPC connection was established`)));
|
|
361
|
+
teardownFailedInit();
|
|
362
|
+
reject(new RPCInitTimeoutError(RPC_INIT_TIMEOUT_MS, createWorkerStartupError(`Worker process exited with code ${code}, signal ${exitSignal} before IPC connection was established`, workerStderrTail)));
|
|
294
363
|
});
|
|
295
364
|
}
|
|
296
365
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { SUPPORTED_AUDIO_FORMATS } from "./constants/audio";
|
|
|
10
10
|
export { InferenceCancelledError } from "./utils/errors-server";
|
|
11
11
|
export type { InferenceCancelledPartial } from "./utils/errors-server";
|
|
12
12
|
export { ContextOverflowError, RequestIdConflictError, RequestNotFoundError, RequestRejectedByPolicyError, } from "./utils/errors-server";
|
|
13
|
-
export { WorkerCrashedError, WorkerShutdownError, } from "./utils/errors-client";
|
|
13
|
+
export { BareRuntimeBinaryNotFoundError, WorkerCrashedError, WorkerShutdownError, } from "./utils/errors-client";
|
|
14
14
|
export { getLogger, SDK_LOG_ID } from "./logging/index";
|
|
15
15
|
export type { Logger, LogTransport, LoggerOptions } from "./logging/index";
|
|
16
16
|
export { profiler } from "./profiling/index";
|
package/dist/index.js
CHANGED
|
@@ -35,8 +35,11 @@ export { ContextOverflowError, RequestIdConflictError, RequestNotFoundError, Req
|
|
|
35
35
|
// `WorkerCrashedError` and `WorkerShutdownError` are thrown by the
|
|
36
36
|
// rpc-client life-signal race when the bare worker exits unexpectedly
|
|
37
37
|
// or close()/process-exit teardown runs while a caller is in flight.
|
|
38
|
-
//
|
|
39
|
-
|
|
38
|
+
// `BareRuntimeBinaryNotFoundError` is thrown when the worker fails to
|
|
39
|
+
// spawn because the platform's `bare-runtime-<platform>-<arch>` package is
|
|
40
|
+
// missing (common under pnpm). Exported so consumers can pattern-match with
|
|
41
|
+
// `instanceof`.
|
|
42
|
+
export { BareRuntimeBinaryNotFoundError, WorkerCrashedError, WorkerShutdownError, } from "./utils/errors-client.js";
|
|
40
43
|
// Logging exports
|
|
41
44
|
export { getLogger, SDK_LOG_ID } from "./logging/index.js";
|
|
42
45
|
// Profiler exports
|
|
@@ -35,6 +35,7 @@ export declare const SDK_CLIENT_ERROR_CODES: {
|
|
|
35
35
|
readonly BARE_PACK_ERROR: 50611;
|
|
36
36
|
readonly INVALID_PLUGIN_SPECIFIER: 50612;
|
|
37
37
|
readonly BARE_IMPORTS_MAP_NOT_FOUND: 50613;
|
|
38
|
+
readonly BARE_RUNTIME_BINARY_NOT_FOUND: 50614;
|
|
38
39
|
readonly PROFILER_INVALID_CAPACITY: 50800;
|
|
39
40
|
};
|
|
40
41
|
declare const clientErrorDefinitions: ErrorCodesMap;
|
|
@@ -40,6 +40,7 @@ export const SDK_CLIENT_ERROR_CODES = {
|
|
|
40
40
|
BARE_PACK_ERROR: 50611,
|
|
41
41
|
INVALID_PLUGIN_SPECIFIER: 50612,
|
|
42
42
|
BARE_IMPORTS_MAP_NOT_FOUND: 50613,
|
|
43
|
+
BARE_RUNTIME_BINARY_NOT_FOUND: 50614,
|
|
43
44
|
// Profiler Errors (50,800-50,899)
|
|
44
45
|
PROFILER_INVALID_CAPACITY: 50800,
|
|
45
46
|
};
|
|
@@ -188,6 +189,10 @@ const clientErrorDefinitions = {
|
|
|
188
189
|
name: "BARE_IMPORTS_MAP_NOT_FOUND",
|
|
189
190
|
message: (sdkName, expectedPath) => `bare-imports.json not found.\n\n Expected at: ${expectedPath}\n\n Make sure ${sdkName} is installed in your project.`,
|
|
190
191
|
},
|
|
192
|
+
[SDK_CLIENT_ERROR_CODES.BARE_RUNTIME_BINARY_NOT_FOUND]: {
|
|
193
|
+
name: "BARE_RUNTIME_BINARY_NOT_FOUND",
|
|
194
|
+
message: (platform, arch) => `Could not load the Bare runtime binary for ${platform}-${arch}. The platform package "bare-runtime-${platform}-${arch}" (or one of its dependencies) is missing from node_modules — commonly seen with pnpm, which does not always install nested optional dependencies. Fix it by installing the platform package directly (e.g. \`pnpm add bare-runtime-${platform}-${arch}\`) or by installing with npm or bun. See https://github.com/tetherto/qvac/issues/1492`,
|
|
195
|
+
},
|
|
191
196
|
// Profiler Errors (50,800-50,899)
|
|
192
197
|
[SDK_CLIENT_ERROR_CODES.PROFILER_INVALID_CAPACITY]: {
|
|
193
198
|
name: "PROFILER_INVALID_CAPACITY",
|
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import { generateStableToolCallId, isValidToolCall, validateToolArguments, } from "../shared.js";
|
|
2
|
+
// Qwen3.5/3.6 can fuse its two tool templates into one frame, embedding the
|
|
3
|
+
// XML `<function=NAME>` token as a bare string key inside the JSON envelope:
|
|
4
|
+
// {"function=NAME","arguments":{...}} (invalid JSON, not parseable as-is)
|
|
5
|
+
// Rewrite only that exact shape to a canonical `{"name":NAME,"arguments":...}`
|
|
6
|
+
// frame. Kept deliberately narrow so well-formed JSON frames are never touched.
|
|
7
|
+
function repairFunctionEqualsJson(candidate) {
|
|
8
|
+
const match = /^\{\s*"function=([^"]+)"\s*,\s*"arguments"\s*:\s*([\s\S]+)\}\s*$/.exec(candidate);
|
|
9
|
+
if (!match)
|
|
10
|
+
return undefined;
|
|
11
|
+
return `{"name":${JSON.stringify(match[1])},"arguments":${match[2]}}`;
|
|
12
|
+
}
|
|
13
|
+
function parseHermesJson(candidate) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(candidate);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const repaired = repairFunctionEqualsJson(candidate);
|
|
19
|
+
if (repaired === undefined)
|
|
20
|
+
throw err;
|
|
21
|
+
return JSON.parse(repaired);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
2
24
|
// Hermes-style: JSON payload wrapped in `<tool_call>...</tool_call>` tags
|
|
3
25
|
export function parseHermesFormat(text, tools) {
|
|
4
26
|
const toolCalls = [];
|
|
@@ -21,7 +43,7 @@ export function parseHermesFormat(text, tools) {
|
|
|
21
43
|
const trimmedJson = callJson.trim();
|
|
22
44
|
let callItem;
|
|
23
45
|
try {
|
|
24
|
-
callItem =
|
|
46
|
+
callItem = parseHermesJson(trimmedJson);
|
|
25
47
|
}
|
|
26
48
|
catch (error) {
|
|
27
49
|
errors.push({
|
|
@@ -69,7 +91,7 @@ function recoverIncompleteHermesFrame(text, tools) {
|
|
|
69
91
|
}
|
|
70
92
|
let parsed;
|
|
71
93
|
try {
|
|
72
|
-
parsed =
|
|
94
|
+
parsed = parseHermesJson(candidate);
|
|
73
95
|
}
|
|
74
96
|
catch {
|
|
75
97
|
return { matched: false, toolCalls: [], errors: [] };
|
|
@@ -94,6 +94,9 @@ export declare class InvalidPluginSpecifierError extends QvacErrorBase {
|
|
|
94
94
|
export declare class BareImportsMapNotFoundError extends QvacErrorBase {
|
|
95
95
|
constructor(sdkName: string, expectedPath: string, cause?: unknown);
|
|
96
96
|
}
|
|
97
|
+
export declare class BareRuntimeBinaryNotFoundError extends QvacErrorBase {
|
|
98
|
+
constructor(platform: string, arch: string, cause?: unknown);
|
|
99
|
+
}
|
|
97
100
|
export declare class ConfigFileNotFoundError extends QvacErrorBase {
|
|
98
101
|
constructor(searchPaths: string, cause?: unknown);
|
|
99
102
|
}
|
|
@@ -166,6 +166,11 @@ export class BareImportsMapNotFoundError extends QvacErrorBase {
|
|
|
166
166
|
super(createErrorOptions(SDK_CLIENT_ERROR_CODES.BARE_IMPORTS_MAP_NOT_FOUND, [sdkName, expectedPath], cause));
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
|
+
export class BareRuntimeBinaryNotFoundError extends QvacErrorBase {
|
|
170
|
+
constructor(platform, arch, cause) {
|
|
171
|
+
super(createErrorOptions(SDK_CLIENT_ERROR_CODES.BARE_RUNTIME_BINARY_NOT_FOUND, [platform, arch], cause));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
169
174
|
export class ConfigFileNotFoundError extends QvacErrorBase {
|
|
170
175
|
constructor(searchPaths, cause) {
|
|
171
176
|
super(createErrorOptions(SDK_CLIENT_ERROR_CODES.CONFIG_FILE_NOT_FOUND, [searchPaths], cause));
|