@mcoda/mswarm 0.1.80 → 0.1.81
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/runtime.js
CHANGED
|
@@ -26,7 +26,12 @@ const DEFAULT_MCODA_LIST_ARGS = ["agent", "list", "--json", "--refresh-health"];
|
|
|
26
26
|
const DEFAULT_COMMAND_MAX_BUFFER = 16 * 1024 * 1024;
|
|
27
27
|
const DEFAULT_LOCAL_ARTIFACT_MAX_BYTES = 512 * 1024 * 1024;
|
|
28
28
|
const DEFAULT_JOB_POLL_WAIT_MS = 25000;
|
|
29
|
+
const DEFAULT_SELF_HOSTED_JOBS_POLL_PATH = "/v1/swarm/self-hosted/node/jobs/poll";
|
|
30
|
+
const DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE = "/v1/swarm/self-hosted/node/jobs/:jobId/start";
|
|
31
|
+
const DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE = "/v1/swarm/self-hosted/node/jobs/:jobId/events";
|
|
32
|
+
const DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE = "/v1/swarm/self-hosted/node/jobs/:jobId/result";
|
|
29
33
|
const DEFAULT_STREAM_EVENT_BATCH_SIZE = 8;
|
|
34
|
+
const SELF_HOSTED_PROTOCOL_MISMATCH_CODE = "self_hosted_protocol_mismatch";
|
|
30
35
|
const OWNER_LOCAL_TEST_ECHO_JOB_TYPE = "tenant.test-echo";
|
|
31
36
|
const TEST_ECHO_RUNNER_ID = "test.echo";
|
|
32
37
|
const RENDER_BLENDER_JOB_TYPE = "render.blender";
|
|
@@ -402,6 +407,37 @@ function parseCliOptions(argv) {
|
|
|
402
407
|
function trimTrailingSlash(value) {
|
|
403
408
|
return value.replace(/\/+$/g, "");
|
|
404
409
|
}
|
|
410
|
+
function lifecyclePath(value, fallback) {
|
|
411
|
+
return optionalText(value) || fallback;
|
|
412
|
+
}
|
|
413
|
+
function resolveLifecycleUrl(gatewayBaseUrl, pathOrUrl) {
|
|
414
|
+
try {
|
|
415
|
+
return new URL(pathOrUrl).toString();
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return new URL(pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`, `${trimTrailingSlash(gatewayBaseUrl)}/`).toString();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function resolveLifecycleTemplate(template, jobId) {
|
|
422
|
+
const encoded = encodeURIComponent(jobId);
|
|
423
|
+
return template
|
|
424
|
+
.replace(/:jobId\b/g, encoded)
|
|
425
|
+
.replace(/:job_id\b/g, encoded)
|
|
426
|
+
.replace(/\{jobId\}/g, encoded)
|
|
427
|
+
.replace(/\{job_id\}/g, encoded);
|
|
428
|
+
}
|
|
429
|
+
function usageFromTokenCounts(promptTokens, completionTokens) {
|
|
430
|
+
const prompt = positiveInteger(promptTokens) ?? 0;
|
|
431
|
+
const completion = positiveInteger(completionTokens) ?? 0;
|
|
432
|
+
return { prompt_tokens: prompt, completion_tokens: completion, total_tokens: prompt + completion };
|
|
433
|
+
}
|
|
434
|
+
function openAiUsage(response) {
|
|
435
|
+
const usage = isRecord(response?.usage) ? response.usage : undefined;
|
|
436
|
+
const prompt = positiveInteger(usage?.prompt_tokens) ?? 0;
|
|
437
|
+
const completion = positiveInteger(usage?.completion_tokens) ?? 0;
|
|
438
|
+
const total = positiveInteger(usage?.total_tokens) ?? prompt + completion;
|
|
439
|
+
return { prompt_tokens: prompt, completion_tokens: completion, total_tokens: total };
|
|
440
|
+
}
|
|
405
441
|
function defaultStatePath() {
|
|
406
442
|
return join(homedir(), ".mswarm", "self-hosted-node", "config.json");
|
|
407
443
|
}
|
|
@@ -631,6 +667,18 @@ function isAgentExposed(agentSlug, defaultModel, bestUsage, config) {
|
|
|
631
667
|
}
|
|
632
668
|
return config.exposeAllModels;
|
|
633
669
|
}
|
|
670
|
+
class HttpStatusError extends Error {
|
|
671
|
+
constructor(url, status, responseText) {
|
|
672
|
+
super(`request_failed:${status}:${responseText.slice(0, 200)}`);
|
|
673
|
+
this.name = "HttpStatusError";
|
|
674
|
+
this.url = url;
|
|
675
|
+
this.status = status;
|
|
676
|
+
this.responseText = responseText;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function isHttpStatusError(error, status) {
|
|
680
|
+
return error instanceof HttpStatusError && (status === undefined || error.status === status);
|
|
681
|
+
}
|
|
634
682
|
async function fetchJson(fetchImpl, url, init, timeoutMs) {
|
|
635
683
|
const controller = new AbortController();
|
|
636
684
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -638,7 +686,7 @@ async function fetchJson(fetchImpl, url, init, timeoutMs) {
|
|
|
638
686
|
const response = await fetchImpl(url, { ...init, signal: controller.signal });
|
|
639
687
|
if (!response.ok) {
|
|
640
688
|
const text = await response.text().catch(() => "");
|
|
641
|
-
throw new
|
|
689
|
+
throw new HttpStatusError(url, response.status, text);
|
|
642
690
|
}
|
|
643
691
|
return (await response.json());
|
|
644
692
|
}
|
|
@@ -1168,6 +1216,18 @@ export async function readSelfHostedNodeConfig(env = process.env) {
|
|
|
1168
1216
|
const maxConcurrentLlmJobs = parsePositiveInteger(env.MSWARM_SELF_HOSTED_MAX_CONCURRENT_LLM_JOBS, state.max_concurrent_llm_jobs || maxConcurrentJobs);
|
|
1169
1217
|
return {
|
|
1170
1218
|
gatewayBaseUrl: trimTrailingSlash(gatewayBaseUrl),
|
|
1219
|
+
jobsPollPath: optionalText(env.MSWARM_SELF_HOSTED_JOBS_POLL_PATH) ||
|
|
1220
|
+
state.jobs_poll_path ||
|
|
1221
|
+
DEFAULT_SELF_HOSTED_JOBS_POLL_PATH,
|
|
1222
|
+
jobsStartPathTemplate: optionalText(env.MSWARM_SELF_HOSTED_JOBS_START_PATH_TEMPLATE) ||
|
|
1223
|
+
state.jobs_start_path_template ||
|
|
1224
|
+
DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE,
|
|
1225
|
+
jobsEventsPathTemplate: optionalText(env.MSWARM_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE) ||
|
|
1226
|
+
state.jobs_events_path_template ||
|
|
1227
|
+
DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE,
|
|
1228
|
+
jobsResultPathTemplate: optionalText(env.MSWARM_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE) ||
|
|
1229
|
+
state.jobs_result_path_template ||
|
|
1230
|
+
DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE,
|
|
1171
1231
|
nodeId,
|
|
1172
1232
|
serverName: optionalText(env.MSWARM_SELF_HOSTED_SERVER_NAME) || state.server_name || null,
|
|
1173
1233
|
relayMode: parseRelayMode(env.MSWARM_SELF_HOSTED_RELAY_MODE || state.relay_mode),
|
|
@@ -1691,6 +1751,7 @@ const PRE_START_JOB_ERROR_CODES = new Set([
|
|
|
1691
1751
|
"validation_failed",
|
|
1692
1752
|
"docdex_context_missing",
|
|
1693
1753
|
"docdex_api_key_missing",
|
|
1754
|
+
SELF_HOSTED_PROTOCOL_MISMATCH_CODE,
|
|
1694
1755
|
]);
|
|
1695
1756
|
class SelfHostedDocdexJobError extends Error {
|
|
1696
1757
|
constructor(code, message) {
|
|
@@ -1706,6 +1767,18 @@ class SelfHostedPreStartJobError extends Error {
|
|
|
1706
1767
|
this.code = code;
|
|
1707
1768
|
}
|
|
1708
1769
|
}
|
|
1770
|
+
class SelfHostedProtocolMismatchError extends Error {
|
|
1771
|
+
constructor(input) {
|
|
1772
|
+
super(`Gateway is missing lifecycle endpoint ${input.endpoint} at ${input.gatewayBaseUrl} ` +
|
|
1773
|
+
`(runtime package version ${input.runtimePackageVersion}). ` +
|
|
1774
|
+
"Run a direct local agent/model instead, for example qwen3.6-llama.cpp.");
|
|
1775
|
+
this.code = SELF_HOSTED_PROTOCOL_MISMATCH_CODE;
|
|
1776
|
+
this.name = SELF_HOSTED_PROTOCOL_MISMATCH_CODE;
|
|
1777
|
+
this.endpoint = input.endpoint;
|
|
1778
|
+
this.gatewayBaseUrl = input.gatewayBaseUrl;
|
|
1779
|
+
this.runtimePackageVersion = input.runtimePackageVersion;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1709
1782
|
function normalizeDocdexCapabilityMap(value) {
|
|
1710
1783
|
const record = objectRecord(value);
|
|
1711
1784
|
if (!record)
|
|
@@ -3214,9 +3287,36 @@ export class McodaLocalAgentExecutor {
|
|
|
3214
3287
|
export class MswarmSelfHostedNodeClient {
|
|
3215
3288
|
constructor(input) {
|
|
3216
3289
|
this.gatewayBaseUrl = trimTrailingSlash(input.gatewayBaseUrl);
|
|
3290
|
+
this.jobsPollPath = lifecyclePath(input.jobsPollPath, DEFAULT_SELF_HOSTED_JOBS_POLL_PATH);
|
|
3291
|
+
this.jobsStartPathTemplate = lifecyclePath(input.jobsStartPathTemplate, DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE);
|
|
3292
|
+
this.jobsEventsPathTemplate = lifecyclePath(input.jobsEventsPathTemplate, DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE);
|
|
3293
|
+
this.jobsResultPathTemplate = lifecyclePath(input.jobsResultPathTemplate, DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE);
|
|
3217
3294
|
this.fetchImpl = input.fetchImpl || fetch;
|
|
3218
3295
|
this.timeoutMs = input.timeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
3219
3296
|
}
|
|
3297
|
+
lifecycleEndpoint(kind) {
|
|
3298
|
+
const path = kind === "poll"
|
|
3299
|
+
? this.jobsPollPath
|
|
3300
|
+
: kind === "start"
|
|
3301
|
+
? this.jobsStartPathTemplate
|
|
3302
|
+
: kind === "events"
|
|
3303
|
+
? this.jobsEventsPathTemplate
|
|
3304
|
+
: this.jobsResultPathTemplate;
|
|
3305
|
+
return `POST ${path}`;
|
|
3306
|
+
}
|
|
3307
|
+
lifecycleGatewayBaseUrl() {
|
|
3308
|
+
return this.gatewayBaseUrl;
|
|
3309
|
+
}
|
|
3310
|
+
lifecycleUrl(kind, jobId) {
|
|
3311
|
+
const path = kind === "poll"
|
|
3312
|
+
? this.jobsPollPath
|
|
3313
|
+
: kind === "start"
|
|
3314
|
+
? this.jobsStartPathTemplate
|
|
3315
|
+
: kind === "events"
|
|
3316
|
+
? this.jobsEventsPathTemplate
|
|
3317
|
+
: this.jobsResultPathTemplate;
|
|
3318
|
+
return resolveLifecycleUrl(this.gatewayBaseUrl, jobId ? resolveLifecycleTemplate(path, jobId) : path);
|
|
3319
|
+
}
|
|
3220
3320
|
async enroll(nodeId, enrollmentToken) {
|
|
3221
3321
|
return fetchJson(this.fetchImpl, `${this.gatewayBaseUrl}/v1/swarm/self-hosted/node/enroll`, {
|
|
3222
3322
|
method: "POST",
|
|
@@ -3268,7 +3368,7 @@ export class MswarmSelfHostedNodeClient {
|
|
|
3268
3368
|
}, this.timeoutMs);
|
|
3269
3369
|
}
|
|
3270
3370
|
async pollJob(runtimeToken, payload) {
|
|
3271
|
-
return fetchJson(this.fetchImpl,
|
|
3371
|
+
return fetchJson(this.fetchImpl, this.lifecycleUrl("poll"), {
|
|
3272
3372
|
method: "POST",
|
|
3273
3373
|
headers: {
|
|
3274
3374
|
"content-type": "application/json",
|
|
@@ -3278,7 +3378,7 @@ export class MswarmSelfHostedNodeClient {
|
|
|
3278
3378
|
}, Math.max(this.timeoutMs, (payload.wait_ms || 0) + 5000));
|
|
3279
3379
|
}
|
|
3280
3380
|
async postJobResult(runtimeToken, jobId, payload) {
|
|
3281
|
-
return fetchJson(this.fetchImpl,
|
|
3381
|
+
return fetchJson(this.fetchImpl, this.lifecycleUrl("result", jobId), {
|
|
3282
3382
|
method: "POST",
|
|
3283
3383
|
headers: {
|
|
3284
3384
|
"content-type": "application/json",
|
|
@@ -3288,7 +3388,7 @@ export class MswarmSelfHostedNodeClient {
|
|
|
3288
3388
|
}, this.timeoutMs);
|
|
3289
3389
|
}
|
|
3290
3390
|
async postJobStart(runtimeToken, jobId, payload) {
|
|
3291
|
-
return fetchJson(this.fetchImpl,
|
|
3391
|
+
return fetchJson(this.fetchImpl, this.lifecycleUrl("start", jobId), {
|
|
3292
3392
|
method: "POST",
|
|
3293
3393
|
headers: {
|
|
3294
3394
|
"content-type": "application/json",
|
|
@@ -3298,7 +3398,7 @@ export class MswarmSelfHostedNodeClient {
|
|
|
3298
3398
|
}, this.timeoutMs);
|
|
3299
3399
|
}
|
|
3300
3400
|
async postJobEvents(runtimeToken, jobId, payload) {
|
|
3301
|
-
return fetchJson(this.fetchImpl,
|
|
3401
|
+
return fetchJson(this.fetchImpl, this.lifecycleUrl("events", jobId), {
|
|
3302
3402
|
method: "POST",
|
|
3303
3403
|
headers: {
|
|
3304
3404
|
"content-type": "application/json",
|
|
@@ -3316,11 +3416,16 @@ export class SelfHostedNodeRuntime {
|
|
|
3316
3416
|
this.queuedGenericJobs = 0;
|
|
3317
3417
|
this.latencySamplesMs = [];
|
|
3318
3418
|
this.recentFailures = [];
|
|
3419
|
+
this.lifecyclePollingDisabled = false;
|
|
3319
3420
|
this.config = config;
|
|
3320
3421
|
this.gateway =
|
|
3321
3422
|
deps?.gateway ||
|
|
3322
3423
|
new MswarmSelfHostedNodeClient({
|
|
3323
3424
|
gatewayBaseUrl: config.gatewayBaseUrl,
|
|
3425
|
+
jobsPollPath: config.jobsPollPath,
|
|
3426
|
+
jobsStartPathTemplate: config.jobsStartPathTemplate,
|
|
3427
|
+
jobsEventsPathTemplate: config.jobsEventsPathTemplate,
|
|
3428
|
+
jobsResultPathTemplate: config.jobsResultPathTemplate,
|
|
3324
3429
|
fetchImpl: deps?.fetchImpl,
|
|
3325
3430
|
timeoutMs: config.requestTimeoutMs
|
|
3326
3431
|
});
|
|
@@ -3395,6 +3500,53 @@ export class SelfHostedNodeRuntime {
|
|
|
3395
3500
|
this.recentFailures.splice(MAX_TELEMETRY_FAILURES);
|
|
3396
3501
|
}
|
|
3397
3502
|
}
|
|
3503
|
+
lifecycleProtocolMismatch(kind, error) {
|
|
3504
|
+
if (!isHttpStatusError(error, 404)) {
|
|
3505
|
+
return undefined;
|
|
3506
|
+
}
|
|
3507
|
+
return new SelfHostedProtocolMismatchError({
|
|
3508
|
+
endpoint: this.gateway.lifecycleEndpoint(kind),
|
|
3509
|
+
gatewayBaseUrl: this.gateway.lifecycleGatewayBaseUrl(),
|
|
3510
|
+
runtimePackageVersion: this.config.nodeVersion
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
isLifecycleProtocolDegradedState(state) {
|
|
3514
|
+
return state?.lifecycle_health_reason === SELF_HOSTED_PROTOCOL_MISMATCH_CODE;
|
|
3515
|
+
}
|
|
3516
|
+
async readLifecycleState() {
|
|
3517
|
+
try {
|
|
3518
|
+
return await readSelfHostedNodeState(this.config.statePath);
|
|
3519
|
+
}
|
|
3520
|
+
catch {
|
|
3521
|
+
return {};
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
async markLifecycleProtocolDegraded(error) {
|
|
3525
|
+
this.lifecyclePollingDisabled = true;
|
|
3526
|
+
this.recentFailures.unshift({
|
|
3527
|
+
execution_class: "agentic",
|
|
3528
|
+
code: SELF_HOSTED_PROTOCOL_MISMATCH_CODE,
|
|
3529
|
+
at: new Date().toISOString()
|
|
3530
|
+
});
|
|
3531
|
+
this.recentFailures.splice(MAX_TELEMETRY_FAILURES);
|
|
3532
|
+
const state = await this.readLifecycleState();
|
|
3533
|
+
await writeSelfHostedNodeState(this.config.statePath, {
|
|
3534
|
+
...state,
|
|
3535
|
+
lifecycle_health_status: "degraded",
|
|
3536
|
+
lifecycle_health_reason: SELF_HOSTED_PROTOCOL_MISMATCH_CODE,
|
|
3537
|
+
lifecycle_health_message: error.message,
|
|
3538
|
+
lifecycle_health_updated_at: new Date().toISOString()
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
jobResultPayload(result) {
|
|
3542
|
+
return {
|
|
3543
|
+
...result,
|
|
3544
|
+
node_id: this.config.nodeId,
|
|
3545
|
+
...(result.status === "success"
|
|
3546
|
+
? { usage: result.usage || openAiUsage(result.openai_response) }
|
|
3547
|
+
: {})
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3398
3550
|
averageLatencyMs(fallback = null) {
|
|
3399
3551
|
if (this.latencySamplesMs.length === 0) {
|
|
3400
3552
|
return fallback;
|
|
@@ -3488,6 +3640,12 @@ export class SelfHostedNodeRuntime {
|
|
|
3488
3640
|
throw new Error("Bootstrap response did not include node_id and runtime_token");
|
|
3489
3641
|
}
|
|
3490
3642
|
const heartbeatInterval = bootstrap.heartbeat_interval_seconds || setupConfig.heartbeatIntervalSeconds;
|
|
3643
|
+
const relay = bootstrap.relay || {};
|
|
3644
|
+
const relayGatewayBaseUrl = optionalText(relay.gateway_base_url) || setupConfig.gatewayBaseUrl;
|
|
3645
|
+
const jobsPollPath = lifecyclePath(relay.jobs_poll_path, DEFAULT_SELF_HOSTED_JOBS_POLL_PATH);
|
|
3646
|
+
const jobsStartPathTemplate = lifecyclePath(relay.jobs_start_path_template, DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE);
|
|
3647
|
+
const jobsEventsPathTemplate = lifecyclePath(relay.jobs_events_path_template, DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE);
|
|
3648
|
+
const jobsResultPathTemplate = lifecyclePath(relay.jobs_result_path_template, DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE);
|
|
3491
3649
|
const state = {
|
|
3492
3650
|
node_id: nodeId,
|
|
3493
3651
|
server_name: optionalText(bootstrap.node?.server_name) || setupConfig.serverName,
|
|
@@ -3501,7 +3659,15 @@ export class SelfHostedNodeRuntime {
|
|
|
3501
3659
|
heartbeat_timeout_seconds: bootstrap.heartbeat_timeout_seconds,
|
|
3502
3660
|
enrolled_at: new Date().toISOString(),
|
|
3503
3661
|
updated_at: new Date().toISOString(),
|
|
3504
|
-
gateway_base_url:
|
|
3662
|
+
gateway_base_url: trimTrailingSlash(relayGatewayBaseUrl),
|
|
3663
|
+
jobs_poll_path: jobsPollPath,
|
|
3664
|
+
jobs_start_path_template: jobsStartPathTemplate,
|
|
3665
|
+
jobs_events_path_template: jobsEventsPathTemplate,
|
|
3666
|
+
jobs_result_path_template: jobsResultPathTemplate,
|
|
3667
|
+
lifecycle_health_status: "healthy",
|
|
3668
|
+
lifecycle_health_reason: undefined,
|
|
3669
|
+
lifecycle_health_message: undefined,
|
|
3670
|
+
lifecycle_health_updated_at: new Date().toISOString(),
|
|
3505
3671
|
ollama_base_url: setupConfig.ollamaBaseUrl,
|
|
3506
3672
|
discovery_mode: setupConfig.discoveryMode,
|
|
3507
3673
|
mcoda_bin: setupConfig.mcodaBin,
|
|
@@ -3525,8 +3691,23 @@ export class SelfHostedNodeRuntime {
|
|
|
3525
3691
|
};
|
|
3526
3692
|
await writeSelfHostedNodeState(setupConfig.statePath, state);
|
|
3527
3693
|
await writeSelfHostedRuntimeToken(setupConfig.runtimeTokenPath, runtimeToken);
|
|
3694
|
+
const runtimeGateway = deps?.gateway && trimTrailingSlash(relayGatewayBaseUrl) === setupConfig.gatewayBaseUrl
|
|
3695
|
+
? deps.gateway
|
|
3696
|
+
: new MswarmSelfHostedNodeClient({
|
|
3697
|
+
gatewayBaseUrl: trimTrailingSlash(relayGatewayBaseUrl),
|
|
3698
|
+
jobsPollPath,
|
|
3699
|
+
jobsStartPathTemplate,
|
|
3700
|
+
jobsEventsPathTemplate,
|
|
3701
|
+
jobsResultPathTemplate,
|
|
3702
|
+
fetchImpl: deps?.fetchImpl,
|
|
3703
|
+
timeoutMs: setupConfig.requestTimeoutMs
|
|
3704
|
+
});
|
|
3528
3705
|
const runtime = new SelfHostedNodeRuntime({
|
|
3529
|
-
gatewayBaseUrl:
|
|
3706
|
+
gatewayBaseUrl: trimTrailingSlash(relayGatewayBaseUrl),
|
|
3707
|
+
jobsPollPath,
|
|
3708
|
+
jobsStartPathTemplate,
|
|
3709
|
+
jobsEventsPathTemplate,
|
|
3710
|
+
jobsResultPathTemplate,
|
|
3530
3711
|
nodeId,
|
|
3531
3712
|
serverName: state.server_name,
|
|
3532
3713
|
relayMode: state.relay_mode || setupConfig.relayMode,
|
|
@@ -3560,7 +3741,7 @@ export class SelfHostedNodeRuntime {
|
|
|
3560
3741
|
exposeAllModels: setupConfig.exposeAllModels,
|
|
3561
3742
|
modelAllowlist: setupConfig.modelAllowlist,
|
|
3562
3743
|
modelBlocklist: setupConfig.modelBlocklist
|
|
3563
|
-
}, { ...deps, gateway });
|
|
3744
|
+
}, { ...deps, gateway: runtimeGateway });
|
|
3564
3745
|
const once = await runtime.runOnce();
|
|
3565
3746
|
return {
|
|
3566
3747
|
created: bootstrap.created === true,
|
|
@@ -3646,6 +3827,12 @@ export class SelfHostedNodeRuntime {
|
|
|
3646
3827
|
if (!runtimeToken) {
|
|
3647
3828
|
throw new Error("Enrollment response did not include runtime_token");
|
|
3648
3829
|
}
|
|
3830
|
+
const relay = response.relay || {};
|
|
3831
|
+
const gatewayBaseUrl = trimTrailingSlash(optionalText(relay.gateway_base_url) || this.config.gatewayBaseUrl);
|
|
3832
|
+
const jobsPollPath = lifecyclePath(relay.jobs_poll_path || this.config.jobsPollPath, DEFAULT_SELF_HOSTED_JOBS_POLL_PATH);
|
|
3833
|
+
const jobsStartPathTemplate = lifecyclePath(relay.jobs_start_path_template || this.config.jobsStartPathTemplate, DEFAULT_SELF_HOSTED_JOBS_START_PATH_TEMPLATE);
|
|
3834
|
+
const jobsEventsPathTemplate = lifecyclePath(relay.jobs_events_path_template || this.config.jobsEventsPathTemplate, DEFAULT_SELF_HOSTED_JOBS_EVENTS_PATH_TEMPLATE);
|
|
3835
|
+
const jobsResultPathTemplate = lifecyclePath(relay.jobs_result_path_template || this.config.jobsResultPathTemplate, DEFAULT_SELF_HOSTED_JOBS_RESULT_PATH_TEMPLATE);
|
|
3649
3836
|
const nextState = {
|
|
3650
3837
|
...currentState,
|
|
3651
3838
|
node_id: this.config.nodeId,
|
|
@@ -3655,7 +3842,15 @@ export class SelfHostedNodeRuntime {
|
|
|
3655
3842
|
heartbeat_timeout_seconds: response.heartbeat_timeout_seconds,
|
|
3656
3843
|
enrolled_at: currentState.enrolled_at || new Date().toISOString(),
|
|
3657
3844
|
updated_at: new Date().toISOString(),
|
|
3658
|
-
gateway_base_url:
|
|
3845
|
+
gateway_base_url: gatewayBaseUrl,
|
|
3846
|
+
jobs_poll_path: jobsPollPath,
|
|
3847
|
+
jobs_start_path_template: jobsStartPathTemplate,
|
|
3848
|
+
jobs_events_path_template: jobsEventsPathTemplate,
|
|
3849
|
+
jobs_result_path_template: jobsResultPathTemplate,
|
|
3850
|
+
lifecycle_health_status: "healthy",
|
|
3851
|
+
lifecycle_health_reason: undefined,
|
|
3852
|
+
lifecycle_health_message: undefined,
|
|
3853
|
+
lifecycle_health_updated_at: new Date().toISOString(),
|
|
3659
3854
|
ollama_base_url: this.config.ollamaBaseUrl,
|
|
3660
3855
|
discovery_mode: this.config.discoveryMode,
|
|
3661
3856
|
mcoda_bin: this.config.mcodaBin,
|
|
@@ -3966,6 +4161,8 @@ export class SelfHostedNodeRuntime {
|
|
|
3966
4161
|
]
|
|
3967
4162
|
});
|
|
3968
4163
|
}
|
|
4164
|
+
const promptTokens = positiveInteger(ollamaResult.promptTokens);
|
|
4165
|
+
const completionTokens = positiveInteger(ollamaResult.completionTokens);
|
|
3969
4166
|
const invocationResult = {
|
|
3970
4167
|
job_id: job.job_id,
|
|
3971
4168
|
request_id: job.request_id,
|
|
@@ -3974,10 +4171,11 @@ export class SelfHostedNodeRuntime {
|
|
|
3974
4171
|
requestId: job.request_id,
|
|
3975
4172
|
model: job.openai_request.model,
|
|
3976
4173
|
content: ollamaResult.content,
|
|
3977
|
-
promptTokens
|
|
3978
|
-
completionTokens
|
|
4174
|
+
promptTokens,
|
|
4175
|
+
completionTokens,
|
|
3979
4176
|
metadata: { provider: "ollama", raw: ollamaResult.raw }
|
|
3980
4177
|
}),
|
|
4178
|
+
usage: usageFromTokenCounts(promptTokens, completionTokens),
|
|
3981
4179
|
...(streamEvents.length ? { stream_events: streamEvents } : {}),
|
|
3982
4180
|
...(progressEvents.length ? { progress_events: progressEvents } : {}),
|
|
3983
4181
|
timing: { local_latency_ms: Date.now() - startedAt }
|
|
@@ -4058,6 +4256,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4058
4256
|
mode: response.metadata.mode
|
|
4059
4257
|
}
|
|
4060
4258
|
}),
|
|
4259
|
+
usage: usageFromTokenCounts(tokens.promptTokens, tokens.completionTokens),
|
|
4061
4260
|
...(streamEvents.length ? { stream_events: streamEvents } : {}),
|
|
4062
4261
|
...(progressEvents.length ? { progress_events: progressEvents } : {}),
|
|
4063
4262
|
timing: { local_latency_ms: Date.now() - startedAt }
|
|
@@ -4124,6 +4323,13 @@ export class SelfHostedNodeRuntime {
|
|
|
4124
4323
|
models = [];
|
|
4125
4324
|
version = null;
|
|
4126
4325
|
}
|
|
4326
|
+
const lifecycleState = await this.readLifecycleState();
|
|
4327
|
+
const lifecycleProtocolDegraded = this.isLifecycleProtocolDegradedState(lifecycleState);
|
|
4328
|
+
if (lifecycleProtocolDegraded) {
|
|
4329
|
+
status = "degraded";
|
|
4330
|
+
recentFailureCount = Math.max(recentFailureCount, 1);
|
|
4331
|
+
this.lifecyclePollingDisabled = true;
|
|
4332
|
+
}
|
|
4127
4333
|
const discoveryLatencyMs = Date.now() - startedAt;
|
|
4128
4334
|
const capabilityPayload = await this.buildCapabilityHeartbeatPayload(enrollment.runtimeToken);
|
|
4129
4335
|
const loadTelemetry = this.buildLoadTelemetry({
|
|
@@ -4165,7 +4371,17 @@ export class SelfHostedNodeRuntime {
|
|
|
4165
4371
|
relay_mode: this.config.relayMode || "outbound",
|
|
4166
4372
|
load_reporting_enabled: loadReportingEnabled,
|
|
4167
4373
|
hardware_telemetry_enabled: this.config.hardwareTelemetryEnabled === true,
|
|
4168
|
-
drain_mode: this.config.drainMode === true
|
|
4374
|
+
drain_mode: this.config.drainMode === true,
|
|
4375
|
+
...(lifecycleProtocolDegraded
|
|
4376
|
+
? {
|
|
4377
|
+
lifecycle: {
|
|
4378
|
+
status: "degraded",
|
|
4379
|
+
reason: lifecycleState.lifecycle_health_reason,
|
|
4380
|
+
message: lifecycleState.lifecycle_health_message,
|
|
4381
|
+
updated_at: lifecycleState.lifecycle_health_updated_at
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
: {})
|
|
4169
4385
|
},
|
|
4170
4386
|
discovery: {
|
|
4171
4387
|
source: discoverySource,
|
|
@@ -4185,7 +4401,14 @@ export class SelfHostedNodeRuntime {
|
|
|
4185
4401
|
avg_latency_ms: loadTelemetry.avg_latency_ms ?? discoveryLatencyMs,
|
|
4186
4402
|
recent_failure_count: loadTelemetry.recent_failure_count,
|
|
4187
4403
|
recent_failures: loadTelemetry.recent_failures,
|
|
4188
|
-
last_success_at: status === "online" ? new Date().toISOString() : null
|
|
4404
|
+
last_success_at: status === "online" ? new Date().toISOString() : null,
|
|
4405
|
+
...(lifecycleProtocolDegraded
|
|
4406
|
+
? {
|
|
4407
|
+
lifecycle_status: "degraded",
|
|
4408
|
+
lifecycle_reason: lifecycleState.lifecycle_health_reason,
|
|
4409
|
+
lifecycle_message: lifecycleState.lifecycle_health_message
|
|
4410
|
+
}
|
|
4411
|
+
: {})
|
|
4189
4412
|
},
|
|
4190
4413
|
local_agent_catalog: {
|
|
4191
4414
|
revision: loadTelemetry.catalog_fingerprint,
|
|
@@ -4239,20 +4462,36 @@ export class SelfHostedNodeRuntime {
|
|
|
4239
4462
|
return { count: models.filter((model) => model.exposed !== false).length, response };
|
|
4240
4463
|
}
|
|
4241
4464
|
async pollAndExecuteJob(waitMs = DEFAULT_JOB_POLL_WAIT_MS) {
|
|
4465
|
+
const lifecycleState = await this.readLifecycleState();
|
|
4466
|
+
if (this.lifecyclePollingDisabled || this.isLifecycleProtocolDegradedState(lifecycleState)) {
|
|
4467
|
+
this.lifecyclePollingDisabled = true;
|
|
4468
|
+
return { executed: false, status: "failed" };
|
|
4469
|
+
}
|
|
4242
4470
|
const enrollment = await this.ensureEnrolled();
|
|
4243
4471
|
const pollCapacity = this.buildLoadTelemetry({ models: [] });
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4472
|
+
let response;
|
|
4473
|
+
try {
|
|
4474
|
+
response = await this.gateway.pollJob(enrollment.runtimeToken, {
|
|
4475
|
+
node_id: this.config.nodeId,
|
|
4476
|
+
capacity: {
|
|
4477
|
+
active_jobs: pollCapacity.active_jobs,
|
|
4478
|
+
queued_jobs: pollCapacity.queued_jobs,
|
|
4479
|
+
max_jobs: pollCapacity.max_concurrency,
|
|
4480
|
+
max_concurrency: pollCapacity.max_concurrency,
|
|
4481
|
+
free_slots: pollCapacity.free_slots,
|
|
4482
|
+
drain_mode: pollCapacity.drain_mode
|
|
4483
|
+
},
|
|
4484
|
+
wait_ms: waitMs
|
|
4485
|
+
});
|
|
4486
|
+
}
|
|
4487
|
+
catch (error) {
|
|
4488
|
+
const mismatch = this.lifecycleProtocolMismatch("poll", error);
|
|
4489
|
+
if (mismatch) {
|
|
4490
|
+
await this.markLifecycleProtocolDegraded(mismatch);
|
|
4491
|
+
return { executed: false, status: "failed" };
|
|
4492
|
+
}
|
|
4493
|
+
throw error;
|
|
4494
|
+
}
|
|
4256
4495
|
const job = response.job || null;
|
|
4257
4496
|
if (!job) {
|
|
4258
4497
|
return { executed: false };
|
|
@@ -4272,19 +4511,35 @@ export class SelfHostedNodeRuntime {
|
|
|
4272
4511
|
});
|
|
4273
4512
|
forwardedStreamEventCount += stream_events.length;
|
|
4274
4513
|
}
|
|
4275
|
-
catch {
|
|
4514
|
+
catch (error) {
|
|
4515
|
+
const mismatch = this.lifecycleProtocolMismatch("events", error);
|
|
4516
|
+
if (mismatch) {
|
|
4517
|
+
streamEventForwardingFailed = true;
|
|
4518
|
+
await this.markLifecycleProtocolDegraded(mismatch);
|
|
4519
|
+
throw mismatch;
|
|
4520
|
+
}
|
|
4276
4521
|
streamEventForwardingFailed = true;
|
|
4277
4522
|
}
|
|
4278
4523
|
};
|
|
4279
|
-
|
|
4524
|
+
let result = await this.executeJob(job, {
|
|
4280
4525
|
attachedMswarmApiKey: optionalText(response.attached_mswarm_api_key) || undefined,
|
|
4281
4526
|
onStarted: async (event) => {
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4527
|
+
try {
|
|
4528
|
+
await this.gateway.postJobStart(enrollment.runtimeToken, job.job_id, {
|
|
4529
|
+
node_id: this.config.nodeId,
|
|
4530
|
+
agent_slug: event.agent_slug || job.agent_slug,
|
|
4531
|
+
source_agent_slug: event.source_agent_slug || job.source_agent_slug || null,
|
|
4532
|
+
model: event.model || job.model || job.openai_request.model
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
catch (error) {
|
|
4536
|
+
const mismatch = this.lifecycleProtocolMismatch("start", error);
|
|
4537
|
+
if (mismatch) {
|
|
4538
|
+
await this.markLifecycleProtocolDegraded(mismatch);
|
|
4539
|
+
throw mismatch;
|
|
4540
|
+
}
|
|
4541
|
+
throw error;
|
|
4542
|
+
}
|
|
4288
4543
|
},
|
|
4289
4544
|
onOpenAIChunk: async (chunk) => {
|
|
4290
4545
|
if (job.openai_request.stream !== true || streamEventForwardingFailed) {
|
|
@@ -4296,7 +4551,23 @@ export class SelfHostedNodeRuntime {
|
|
|
4296
4551
|
}
|
|
4297
4552
|
},
|
|
4298
4553
|
});
|
|
4299
|
-
|
|
4554
|
+
try {
|
|
4555
|
+
await flushStreamEvents();
|
|
4556
|
+
}
|
|
4557
|
+
catch (error) {
|
|
4558
|
+
if (!(error instanceof SelfHostedProtocolMismatchError)) {
|
|
4559
|
+
throw error;
|
|
4560
|
+
}
|
|
4561
|
+
result = {
|
|
4562
|
+
job_id: job.job_id,
|
|
4563
|
+
request_id: job.request_id,
|
|
4564
|
+
status: "failed",
|
|
4565
|
+
error: { code: SELF_HOSTED_PROTOCOL_MISMATCH_CODE, message: error.message },
|
|
4566
|
+
...(result.stream_events?.length ? { stream_events: result.stream_events } : {}),
|
|
4567
|
+
...(result.progress_events?.length ? { progress_events: result.progress_events } : {}),
|
|
4568
|
+
timing: result.timing
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
4300
4571
|
const postedResult = job.openai_request.stream === true
|
|
4301
4572
|
? streamEventForwardingFailed
|
|
4302
4573
|
? {
|
|
@@ -4305,10 +4576,17 @@ export class SelfHostedNodeRuntime {
|
|
|
4305
4576
|
}
|
|
4306
4577
|
: (({ stream_events: _streamEvents, ...rest }) => rest)(result)
|
|
4307
4578
|
: result;
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4579
|
+
try {
|
|
4580
|
+
await this.gateway.postJobResult(enrollment.runtimeToken, job.job_id, this.jobResultPayload(postedResult));
|
|
4581
|
+
}
|
|
4582
|
+
catch (error) {
|
|
4583
|
+
const mismatch = this.lifecycleProtocolMismatch("result", error);
|
|
4584
|
+
if (mismatch) {
|
|
4585
|
+
await this.markLifecycleProtocolDegraded(mismatch);
|
|
4586
|
+
return { executed: true, job_id: job.job_id, status: "failed" };
|
|
4587
|
+
}
|
|
4588
|
+
throw error;
|
|
4589
|
+
}
|
|
4312
4590
|
return { executed: true, job_id: job.job_id, status: result.status };
|
|
4313
4591
|
}
|
|
4314
4592
|
async doctor() {
|
|
@@ -4350,7 +4628,7 @@ export class SelfHostedNodeRuntime {
|
|
|
4350
4628
|
let timer = null;
|
|
4351
4629
|
let polling = false;
|
|
4352
4630
|
const poll = () => {
|
|
4353
|
-
if (stopped || polling || this.config.relayMode === "direct")
|
|
4631
|
+
if (stopped || polling || this.lifecyclePollingDisabled || this.config.relayMode === "direct")
|
|
4354
4632
|
return;
|
|
4355
4633
|
polling = true;
|
|
4356
4634
|
void this.pollAndExecuteJob()
|