@semiont/jobs 0.5.11 → 0.5.13
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/index.d.ts +2 -1
- package/dist/index.js +44 -9
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +155 -23
- package/dist/worker-main.js.map +1 -1
- package/package.json +9 -9
package/dist/worker-main.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTomlConfigLoader, didToAgent, baseUrl, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
|
|
1
|
+
import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
|
|
2
2
|
import { deriveStorageUri } from '@semiont/content';
|
|
3
3
|
import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
|
|
4
4
|
import { generateAnnotationId } from '@semiont/event-sourcing';
|
|
@@ -9226,6 +9226,12 @@ function createJobClaimAdapter(options) {
|
|
|
9226
9226
|
const errors$ = new import_rxjs.Subject();
|
|
9227
9227
|
let jobSubscription = null;
|
|
9228
9228
|
let started = false;
|
|
9229
|
+
let lastQueuedEventAt = null;
|
|
9230
|
+
let lastClaimAt = null;
|
|
9231
|
+
let lastFinishedAt = null;
|
|
9232
|
+
let lastActivityAt = null;
|
|
9233
|
+
let activeSince = null;
|
|
9234
|
+
const iso = (t) => t === null ? null : new Date(t).toISOString();
|
|
9229
9235
|
const claimJob = async (assignment) => {
|
|
9230
9236
|
try {
|
|
9231
9237
|
const job = await busRequest(requestBus, "job:claim", { jobId: assignment.jobId }, 1e4);
|
|
@@ -9250,12 +9256,17 @@ function createJobClaimAdapter(options) {
|
|
|
9250
9256
|
started = true;
|
|
9251
9257
|
bus.addChannels?.(["job:queued"]);
|
|
9252
9258
|
jobSubscription = bus.on$("job:queued").subscribe((event) => {
|
|
9259
|
+
lastQueuedEventAt = Date.now();
|
|
9253
9260
|
const jobType = event.jobType;
|
|
9254
9261
|
if (jobTypes.length > 0 && !jobTypes.includes(jobType)) return;
|
|
9255
9262
|
if (isProcessing$.getValue()) return;
|
|
9256
9263
|
isProcessing$.next(true);
|
|
9257
9264
|
claimJob({ jobId: event.jobId, type: jobType, resourceId: event.resourceId }).then((job) => {
|
|
9258
9265
|
if (job) {
|
|
9266
|
+
const now = Date.now();
|
|
9267
|
+
lastClaimAt = now;
|
|
9268
|
+
lastActivityAt = now;
|
|
9269
|
+
activeSince = now;
|
|
9259
9270
|
activeJob$.next(job);
|
|
9260
9271
|
} else {
|
|
9261
9272
|
isProcessing$.next(false);
|
|
@@ -9271,15 +9282,37 @@ function createJobClaimAdapter(options) {
|
|
|
9271
9282
|
started = false;
|
|
9272
9283
|
},
|
|
9273
9284
|
completeJob: () => {
|
|
9285
|
+
const now = Date.now();
|
|
9286
|
+
lastFinishedAt = now;
|
|
9287
|
+
lastActivityAt = now;
|
|
9288
|
+
activeSince = null;
|
|
9274
9289
|
activeJob$.next(null);
|
|
9275
9290
|
isProcessing$.next(false);
|
|
9276
9291
|
jobsCompleted$.next(jobsCompleted$.getValue() + 1);
|
|
9277
9292
|
},
|
|
9278
9293
|
failJob: (jid, error) => {
|
|
9294
|
+
const now = Date.now();
|
|
9295
|
+
lastFinishedAt = now;
|
|
9296
|
+
lastActivityAt = now;
|
|
9297
|
+
activeSince = null;
|
|
9279
9298
|
activeJob$.next(null);
|
|
9280
9299
|
isProcessing$.next(false);
|
|
9281
9300
|
errors$.next({ jobId: jid, error });
|
|
9282
9301
|
},
|
|
9302
|
+
vitals: () => {
|
|
9303
|
+
const active = activeJob$.getValue();
|
|
9304
|
+
return {
|
|
9305
|
+
lastQueuedEventAt: iso(lastQueuedEventAt),
|
|
9306
|
+
lastClaimAt: iso(lastClaimAt),
|
|
9307
|
+
lastFinishedAt: iso(lastFinishedAt),
|
|
9308
|
+
lastActivityAt: iso(lastActivityAt),
|
|
9309
|
+
activeJob: active && activeSince !== null ? { jobId: active.jobId, type: active.type, since: iso(activeSince) } : null,
|
|
9310
|
+
jobsCompleted: jobsCompleted$.getValue()
|
|
9311
|
+
};
|
|
9312
|
+
},
|
|
9313
|
+
touchActivity: () => {
|
|
9314
|
+
lastActivityAt = Date.now();
|
|
9315
|
+
},
|
|
9283
9316
|
dispose: () => {
|
|
9284
9317
|
jobSubscription?.unsubscribe();
|
|
9285
9318
|
jobSubscription = null;
|
|
@@ -9291,6 +9324,41 @@ function createJobClaimAdapter(options) {
|
|
|
9291
9324
|
}
|
|
9292
9325
|
};
|
|
9293
9326
|
}
|
|
9327
|
+
|
|
9328
|
+
// src/workers/inference-call.ts
|
|
9329
|
+
var INFERENCE_TIMEOUT_MS = 10 * 6e4;
|
|
9330
|
+
async function withTimeout(work, label) {
|
|
9331
|
+
let timer;
|
|
9332
|
+
const timedOut = new Promise((_, reject) => {
|
|
9333
|
+
timer = setTimeout(() => {
|
|
9334
|
+
reject(new Error(
|
|
9335
|
+
`Inference call timed out after ${INFERENCE_TIMEOUT_MS / 6e4} minutes (${label}) \u2014 failing the job to keep the claim loop live`
|
|
9336
|
+
));
|
|
9337
|
+
}, INFERENCE_TIMEOUT_MS);
|
|
9338
|
+
timer.unref?.();
|
|
9339
|
+
});
|
|
9340
|
+
try {
|
|
9341
|
+
return await Promise.race([work, timedOut]);
|
|
9342
|
+
} catch (err) {
|
|
9343
|
+
work.catch(() => {
|
|
9344
|
+
});
|
|
9345
|
+
throw err;
|
|
9346
|
+
} finally {
|
|
9347
|
+
clearTimeout(timer);
|
|
9348
|
+
}
|
|
9349
|
+
}
|
|
9350
|
+
function boundedGenerate(client, prompt, maxTokens, temperature, options) {
|
|
9351
|
+
return withTimeout(
|
|
9352
|
+
client.generateText(prompt, maxTokens, temperature, options),
|
|
9353
|
+
`${client.type}:${client.modelId}`
|
|
9354
|
+
);
|
|
9355
|
+
}
|
|
9356
|
+
function boundedGenerateWithMetadata(client, prompt, maxTokens, temperature, options) {
|
|
9357
|
+
return withTimeout(
|
|
9358
|
+
client.generateTextWithMetadata(prompt, maxTokens, temperature, options),
|
|
9359
|
+
`${client.type}:${client.modelId}`
|
|
9360
|
+
);
|
|
9361
|
+
}
|
|
9294
9362
|
function languageName(tag) {
|
|
9295
9363
|
return getLocaleEnglishName(tag) || tag;
|
|
9296
9364
|
}
|
|
@@ -9772,7 +9840,7 @@ var AnnotationDetection = class {
|
|
|
9772
9840
|
*/
|
|
9773
9841
|
static async detectComments(content, client, instructions, tone, density, language, sourceLanguage) {
|
|
9774
9842
|
const prompt = MotivationPrompts.buildCommentPrompt(content, instructions, tone, density, language, sourceLanguage);
|
|
9775
|
-
const response = await client
|
|
9843
|
+
const response = await boundedGenerateWithMetadata(client, prompt, 3e3, 0.4, { format: "json" });
|
|
9776
9844
|
assertNotTruncated(response, "comment");
|
|
9777
9845
|
return MotivationParsers.parseComments(response.text, content);
|
|
9778
9846
|
}
|
|
@@ -9785,7 +9853,7 @@ var AnnotationDetection = class {
|
|
|
9785
9853
|
*/
|
|
9786
9854
|
static async detectHighlights(content, client, instructions, density, sourceLanguage) {
|
|
9787
9855
|
const prompt = MotivationPrompts.buildHighlightPrompt(content, instructions, density, sourceLanguage);
|
|
9788
|
-
const response = await client
|
|
9856
|
+
const response = await boundedGenerateWithMetadata(client, prompt, 2e3, 0.3, { format: "json" });
|
|
9789
9857
|
assertNotTruncated(response, "highlight");
|
|
9790
9858
|
return MotivationParsers.parseHighlights(response.text, content);
|
|
9791
9859
|
}
|
|
@@ -9798,7 +9866,7 @@ var AnnotationDetection = class {
|
|
|
9798
9866
|
*/
|
|
9799
9867
|
static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage) {
|
|
9800
9868
|
const prompt = MotivationPrompts.buildAssessmentPrompt(content, instructions, tone, density, language, sourceLanguage);
|
|
9801
|
-
const response = await client
|
|
9869
|
+
const response = await boundedGenerateWithMetadata(client, prompt, 3e3, 0.3, { format: "json" });
|
|
9802
9870
|
assertNotTruncated(response, "assessment");
|
|
9803
9871
|
return MotivationParsers.parseAssessments(response.text, content);
|
|
9804
9872
|
}
|
|
@@ -9829,7 +9897,7 @@ var AnnotationDetection = class {
|
|
|
9829
9897
|
categoryInfo.examples,
|
|
9830
9898
|
sourceLanguage
|
|
9831
9899
|
);
|
|
9832
|
-
const response = await client
|
|
9900
|
+
const response = await boundedGenerateWithMetadata(client, prompt, 4e3, 0.2, { format: "json" });
|
|
9833
9901
|
assertNotTruncated(response, "tag");
|
|
9834
9902
|
const parsedTags = MotivationParsers.parseTags(response.text);
|
|
9835
9903
|
return MotivationParsers.validateTagOffsets(parsedTags, content, category);
|
|
@@ -9885,7 +9953,8 @@ If no entities are found, respond with an empty array [].
|
|
|
9885
9953
|
Example output:
|
|
9886
9954
|
[{"exact":"Alice","entityType":"Person","prefix":"","suffix":" went to"},{"exact":"Paris","entityType":"Location","prefix":"went to ","suffix":" yesterday"}]`;
|
|
9887
9955
|
logger2.debug("Sending entity extraction request", { entityTypes: entityTypesDescription });
|
|
9888
|
-
const response = await
|
|
9956
|
+
const response = await boundedGenerateWithMetadata(
|
|
9957
|
+
client,
|
|
9889
9958
|
prompt,
|
|
9890
9959
|
4e3,
|
|
9891
9960
|
// Increased to handle many entities without truncation
|
|
@@ -10135,7 +10204,7 @@ ${formatRequirements}`;
|
|
|
10135
10204
|
temperature: finalTemperature,
|
|
10136
10205
|
maxTokens: finalMaxTokens
|
|
10137
10206
|
});
|
|
10138
|
-
const response = await client
|
|
10207
|
+
const response = await boundedGenerate(client, prompt, finalMaxTokens, finalTemperature);
|
|
10139
10208
|
logger2.debug("Got response from inference", { responseLength: response.length });
|
|
10140
10209
|
const result = parseResponse(response);
|
|
10141
10210
|
logger2.debug("Parsed response", {
|
|
@@ -10495,10 +10564,9 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10495
10564
|
`Unsupported outputMediaType for generation: ${outputMediaType}. Generation produces ${GENERATABLE_MEDIA_TYPES.join(" or ")}.`
|
|
10496
10565
|
);
|
|
10497
10566
|
}
|
|
10498
|
-
onProgress(20, "Fetching context...", "fetching");
|
|
10499
10567
|
const title = params.title ?? "Untitled";
|
|
10500
10568
|
const entityTypes = (params.entityTypes ?? []).map(String);
|
|
10501
|
-
onProgress(
|
|
10569
|
+
onProgress(5, "Generating resource...", "generating");
|
|
10502
10570
|
const generated = await generateResourceFromTopic(
|
|
10503
10571
|
title,
|
|
10504
10572
|
entityTypes,
|
|
@@ -10522,7 +10590,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10522
10590
|
content = resolved.content;
|
|
10523
10591
|
citations = resolved.citations;
|
|
10524
10592
|
}
|
|
10525
|
-
onProgress(
|
|
10593
|
+
onProgress(95, "Creating resource...", "creating");
|
|
10526
10594
|
return {
|
|
10527
10595
|
content,
|
|
10528
10596
|
title: generated.title ?? title,
|
|
@@ -10619,6 +10687,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10619
10687
|
}
|
|
10620
10688
|
}
|
|
10621
10689
|
const onProgress = (percentage, message, stage, extra) => {
|
|
10690
|
+
adapter.touchActivity();
|
|
10622
10691
|
emitEvent(session, "job:report-progress", {
|
|
10623
10692
|
...lifecycleBase,
|
|
10624
10693
|
percentage,
|
|
@@ -10787,6 +10856,43 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10787
10856
|
|
|
10788
10857
|
// src/worker-runtime.ts
|
|
10789
10858
|
var import_rxjs2 = __toESM(require_cjs());
|
|
10859
|
+
function buildHealthPayload(workers) {
|
|
10860
|
+
return {
|
|
10861
|
+
status: "ok",
|
|
10862
|
+
agents: workers.length,
|
|
10863
|
+
workers: workers.map((w) => w.vitals())
|
|
10864
|
+
};
|
|
10865
|
+
}
|
|
10866
|
+
var STALL_THRESHOLD_MS = 15 * 6e4;
|
|
10867
|
+
var STALL_CHECK_INTERVAL_MS = 6e4;
|
|
10868
|
+
function startStallWatchdog(opts) {
|
|
10869
|
+
const { workers, logger: logger2, exit = (code) => process.exit(code) } = opts;
|
|
10870
|
+
const timer = setInterval(() => {
|
|
10871
|
+
const now = Date.now();
|
|
10872
|
+
for (const worker of workers) {
|
|
10873
|
+
const v = worker.vitals();
|
|
10874
|
+
if (!v.activeJob || !v.lastActivityAt) continue;
|
|
10875
|
+
const silentForMs = now - Date.parse(v.lastActivityAt);
|
|
10876
|
+
if (silentForMs <= STALL_THRESHOLD_MS) continue;
|
|
10877
|
+
logger2.error("Worker stalled \u2014 exiting for restart", {
|
|
10878
|
+
provider: v.provider,
|
|
10879
|
+
model: v.model,
|
|
10880
|
+
did: v.did,
|
|
10881
|
+
jobId: v.activeJob.jobId,
|
|
10882
|
+
jobType: v.activeJob.type,
|
|
10883
|
+
processingSince: v.activeJob.since,
|
|
10884
|
+
lastActivityAt: v.lastActivityAt,
|
|
10885
|
+
silentForMs,
|
|
10886
|
+
thresholdMs: STALL_THRESHOLD_MS
|
|
10887
|
+
});
|
|
10888
|
+
clearInterval(timer);
|
|
10889
|
+
exit(1);
|
|
10890
|
+
return;
|
|
10891
|
+
}
|
|
10892
|
+
}, STALL_CHECK_INTERVAL_MS);
|
|
10893
|
+
timer.unref?.();
|
|
10894
|
+
return { dispose: () => clearInterval(timer) };
|
|
10895
|
+
}
|
|
10790
10896
|
function parseBackendUrl(url) {
|
|
10791
10897
|
const parsed = new URL(url);
|
|
10792
10898
|
const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
|
|
@@ -10795,19 +10901,34 @@ function parseBackendUrl(url) {
|
|
|
10795
10901
|
return { protocol, host, port };
|
|
10796
10902
|
}
|
|
10797
10903
|
async function authenticateAgent(opts) {
|
|
10798
|
-
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model } = opts;
|
|
10904
|
+
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
|
|
10799
10905
|
if (!workerSecret2) {
|
|
10800
10906
|
throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
|
|
10801
10907
|
}
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10908
|
+
return retryWithBackoff(
|
|
10909
|
+
async () => {
|
|
10910
|
+
const response = await fetch(`${backendBaseUrl2}/api/tokens/agent`, {
|
|
10911
|
+
method: "POST",
|
|
10912
|
+
headers: { "Content-Type": "application/json" },
|
|
10913
|
+
body: JSON.stringify({ secret: workerSecret2, provider, model })
|
|
10914
|
+
});
|
|
10915
|
+
if (!response.ok) {
|
|
10916
|
+
throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
|
|
10917
|
+
}
|
|
10918
|
+
return await response.json();
|
|
10919
|
+
},
|
|
10920
|
+
isTransientFetchError,
|
|
10921
|
+
retry,
|
|
10922
|
+
({ attempt, attempts, delayMs, error }) => {
|
|
10923
|
+
logger2?.warn("Backend unreachable, retrying agent authentication", {
|
|
10924
|
+
agent: `${provider}:${model}`,
|
|
10925
|
+
attempt,
|
|
10926
|
+
attempts,
|
|
10927
|
+
retryInMs: delayMs,
|
|
10928
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10929
|
+
});
|
|
10930
|
+
}
|
|
10931
|
+
);
|
|
10811
10932
|
}
|
|
10812
10933
|
async function startAgentWorker(opts) {
|
|
10813
10934
|
const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
|
|
@@ -10817,7 +10938,8 @@ async function startAgentWorker(opts) {
|
|
|
10817
10938
|
backendBaseUrl: backendBaseUrl2,
|
|
10818
10939
|
workerSecret: workerSecret2,
|
|
10819
10940
|
provider: inference.type,
|
|
10820
|
-
model: inference.model
|
|
10941
|
+
model: inference.model,
|
|
10942
|
+
logger: logger2
|
|
10821
10943
|
});
|
|
10822
10944
|
const generator = didToAgent(did);
|
|
10823
10945
|
const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
|
|
@@ -10850,7 +10972,8 @@ async function startAgentWorker(opts) {
|
|
|
10850
10972
|
backendBaseUrl: backendBaseUrl2,
|
|
10851
10973
|
workerSecret: workerSecret2,
|
|
10852
10974
|
provider: inference.type,
|
|
10853
|
-
model: inference.model
|
|
10975
|
+
model: inference.model,
|
|
10976
|
+
logger: logger2
|
|
10854
10977
|
});
|
|
10855
10978
|
return token;
|
|
10856
10979
|
} catch (err) {
|
|
@@ -10881,6 +11004,13 @@ async function startAgentWorker(opts) {
|
|
|
10881
11004
|
});
|
|
10882
11005
|
return {
|
|
10883
11006
|
session,
|
|
11007
|
+
vitals: () => ({
|
|
11008
|
+
provider: inference.type,
|
|
11009
|
+
model: inference.model,
|
|
11010
|
+
did,
|
|
11011
|
+
jobTypes: group.jobTypes,
|
|
11012
|
+
...adapter.vitals()
|
|
11013
|
+
}),
|
|
10884
11014
|
dispose: async () => {
|
|
10885
11015
|
adapter.dispose();
|
|
10886
11016
|
await session.dispose();
|
|
@@ -10973,7 +11103,7 @@ async function main() {
|
|
|
10973
11103
|
const health = createServer((req, res) => {
|
|
10974
11104
|
if (req.url === "/health") {
|
|
10975
11105
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10976
|
-
res.end(JSON.stringify(
|
|
11106
|
+
res.end(JSON.stringify(buildHealthPayload(workers)));
|
|
10977
11107
|
} else {
|
|
10978
11108
|
res.writeHead(404);
|
|
10979
11109
|
res.end();
|
|
@@ -10982,8 +11112,10 @@ async function main() {
|
|
|
10982
11112
|
health.listen(healthPort, () => {
|
|
10983
11113
|
logger.info("Health endpoint ready", { port: healthPort });
|
|
10984
11114
|
});
|
|
11115
|
+
const watchdog = startStallWatchdog({ workers, logger });
|
|
10985
11116
|
const shutdown = async () => {
|
|
10986
11117
|
logger.info("Shutting down");
|
|
11118
|
+
watchdog.dispose();
|
|
10987
11119
|
await Promise.all(workers.map((w) => w.dispose()));
|
|
10988
11120
|
health.close();
|
|
10989
11121
|
process.exit(0);
|