@semiont/jobs 0.5.12 → 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.js +42 -6
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +153 -20
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
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", {
|
|
@@ -10618,6 +10687,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10618
10687
|
}
|
|
10619
10688
|
}
|
|
10620
10689
|
const onProgress = (percentage, message, stage, extra) => {
|
|
10690
|
+
adapter.touchActivity();
|
|
10621
10691
|
emitEvent(session, "job:report-progress", {
|
|
10622
10692
|
...lifecycleBase,
|
|
10623
10693
|
percentage,
|
|
@@ -10786,6 +10856,43 @@ async function handleJobInner(adapter, config, job) {
|
|
|
10786
10856
|
|
|
10787
10857
|
// src/worker-runtime.ts
|
|
10788
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
|
+
}
|
|
10789
10896
|
function parseBackendUrl(url) {
|
|
10790
10897
|
const parsed = new URL(url);
|
|
10791
10898
|
const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
|
|
@@ -10794,19 +10901,34 @@ function parseBackendUrl(url) {
|
|
|
10794
10901
|
return { protocol, host, port };
|
|
10795
10902
|
}
|
|
10796
10903
|
async function authenticateAgent(opts) {
|
|
10797
|
-
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model } = opts;
|
|
10904
|
+
const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
|
|
10798
10905
|
if (!workerSecret2) {
|
|
10799
10906
|
throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
|
|
10800
10907
|
}
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
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
|
+
);
|
|
10810
10932
|
}
|
|
10811
10933
|
async function startAgentWorker(opts) {
|
|
10812
10934
|
const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
|
|
@@ -10816,7 +10938,8 @@ async function startAgentWorker(opts) {
|
|
|
10816
10938
|
backendBaseUrl: backendBaseUrl2,
|
|
10817
10939
|
workerSecret: workerSecret2,
|
|
10818
10940
|
provider: inference.type,
|
|
10819
|
-
model: inference.model
|
|
10941
|
+
model: inference.model,
|
|
10942
|
+
logger: logger2
|
|
10820
10943
|
});
|
|
10821
10944
|
const generator = didToAgent(did);
|
|
10822
10945
|
const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
|
|
@@ -10849,7 +10972,8 @@ async function startAgentWorker(opts) {
|
|
|
10849
10972
|
backendBaseUrl: backendBaseUrl2,
|
|
10850
10973
|
workerSecret: workerSecret2,
|
|
10851
10974
|
provider: inference.type,
|
|
10852
|
-
model: inference.model
|
|
10975
|
+
model: inference.model,
|
|
10976
|
+
logger: logger2
|
|
10853
10977
|
});
|
|
10854
10978
|
return token;
|
|
10855
10979
|
} catch (err) {
|
|
@@ -10880,6 +11004,13 @@ async function startAgentWorker(opts) {
|
|
|
10880
11004
|
});
|
|
10881
11005
|
return {
|
|
10882
11006
|
session,
|
|
11007
|
+
vitals: () => ({
|
|
11008
|
+
provider: inference.type,
|
|
11009
|
+
model: inference.model,
|
|
11010
|
+
did,
|
|
11011
|
+
jobTypes: group.jobTypes,
|
|
11012
|
+
...adapter.vitals()
|
|
11013
|
+
}),
|
|
10883
11014
|
dispose: async () => {
|
|
10884
11015
|
adapter.dispose();
|
|
10885
11016
|
await session.dispose();
|
|
@@ -10972,7 +11103,7 @@ async function main() {
|
|
|
10972
11103
|
const health = createServer((req, res) => {
|
|
10973
11104
|
if (req.url === "/health") {
|
|
10974
11105
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
10975
|
-
res.end(JSON.stringify(
|
|
11106
|
+
res.end(JSON.stringify(buildHealthPayload(workers)));
|
|
10976
11107
|
} else {
|
|
10977
11108
|
res.writeHead(404);
|
|
10978
11109
|
res.end();
|
|
@@ -10981,8 +11112,10 @@ async function main() {
|
|
|
10981
11112
|
health.listen(healthPort, () => {
|
|
10982
11113
|
logger.info("Health endpoint ready", { port: healthPort });
|
|
10983
11114
|
});
|
|
11115
|
+
const watchdog = startStallWatchdog({ workers, logger });
|
|
10984
11116
|
const shutdown = async () => {
|
|
10985
11117
|
logger.info("Shutting down");
|
|
11118
|
+
watchdog.dispose();
|
|
10986
11119
|
await Promise.all(workers.map((w) => w.dispose()));
|
|
10987
11120
|
health.close();
|
|
10988
11121
|
process.exit(0);
|