@semiont/jobs 0.5.28 → 0.5.29
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/README.md +4 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +61 -26
- package/dist/worker-main.js.map +1 -1
- package/package.json +11 -11
package/dist/worker-main.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, isGenerationJobParams, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
|
|
2
|
-
import {
|
|
1
|
+
import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, resourceId, getPrimaryMediaType, isGenerationJobParams, capabilitiesOf, assembleAnnotation, findClaimSpan, textExtractionOf, reconcileSelector, GENERATABLE_MEDIA_TYPES, locate, createFragmentSelector, getLocaleEnglishName, estimateTokens, chunkText, isObject, isString, deriveViews } from '@semiont/core';
|
|
2
|
+
import { archivistContentReads, extractPdfTextLayer, EXTRACTORS, calculateChecksum, withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
|
|
3
3
|
import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
|
|
4
4
|
import { execFileSync } from 'child_process';
|
|
5
5
|
import { existsSync, readFileSync, mkdtempSync, writeFileSync, rmSync } from 'fs';
|
|
6
6
|
import { homedir, hostname, tmpdir } from 'os';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { generateAnnotationId } from '@semiont/event-sourcing';
|
|
9
|
-
import { InMemorySessionStorage, setStoredSession,
|
|
9
|
+
import { InMemorySessionStorage, setStoredSession, kbGatewayUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
|
|
10
10
|
import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
|
|
11
11
|
import { createInferenceClient } from '@semiont/inference';
|
|
12
12
|
import { createServer } from 'http';
|
|
@@ -10995,10 +10995,10 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
|
|
|
10995
10995
|
}
|
|
10996
10996
|
|
|
10997
10997
|
// src/workers/detection/prepare-detection.ts
|
|
10998
|
-
async function prepareDetection(mediaType,
|
|
10998
|
+
async function prepareDetection(mediaType, content, resourceId, userId, generator, store) {
|
|
10999
10999
|
const extractor = EXTRACTORS[textExtractionOf(mediaType)];
|
|
11000
11000
|
if (!extractor) return { declined: "no-extractor" };
|
|
11001
|
-
const { data } = await
|
|
11001
|
+
const { data } = await content.getBinary(resourceId);
|
|
11002
11002
|
const bytes = Buffer.from(data);
|
|
11003
11003
|
const extracted = await extractor.extract(bytes, mediaType, {
|
|
11004
11004
|
key: calculateChecksum(bytes),
|
|
@@ -11114,7 +11114,7 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11114
11114
|
const mediaType = getPrimaryMediaType(descriptor);
|
|
11115
11115
|
const source = await withSpan(
|
|
11116
11116
|
"detection:prepare",
|
|
11117
|
-
() => prepareDetection(mediaType ?? "",
|
|
11117
|
+
() => prepareDetection(mediaType ?? "", config.contentReads, resourceId$1, userId, generator, config.anchoredTextStore),
|
|
11118
11118
|
{ attrs: { "resource.id": resourceId$1, "media.type": mediaType ?? "unknown" } }
|
|
11119
11119
|
);
|
|
11120
11120
|
if ("declined" in source) {
|
|
@@ -11243,7 +11243,16 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11243
11243
|
);
|
|
11244
11244
|
const genParams = job.params;
|
|
11245
11245
|
const genReferenceId = referenceIdOf(job);
|
|
11246
|
-
const storageUri =
|
|
11246
|
+
const storageUri = job.params.storageUri;
|
|
11247
|
+
const expectedExtension = capabilitiesOf(genResult.format)?.extension;
|
|
11248
|
+
if (expectedExtension && !storageUri.toLowerCase().endsWith(expectedExtension)) {
|
|
11249
|
+
config.logger.warn("Storage URI extension does not match the generated format \u2014 writing it as requested", {
|
|
11250
|
+
jobId,
|
|
11251
|
+
storageUri,
|
|
11252
|
+
format: genResult.format,
|
|
11253
|
+
expectedExtension
|
|
11254
|
+
});
|
|
11255
|
+
}
|
|
11247
11256
|
const { resourceId: newResourceId } = await session.client.yield.resource({
|
|
11248
11257
|
name: genResult.title,
|
|
11249
11258
|
file: Buffer.from(genResult.content),
|
|
@@ -11328,6 +11337,29 @@ async function handleJobInner(adapter, config, job) {
|
|
|
11328
11337
|
}
|
|
11329
11338
|
}
|
|
11330
11339
|
|
|
11340
|
+
// src/anchored-text-over-bus.ts
|
|
11341
|
+
function anchoredTextOverBus(client, logger2) {
|
|
11342
|
+
return {
|
|
11343
|
+
async read(key) {
|
|
11344
|
+
try {
|
|
11345
|
+
return await client.browse.anchoredTextByChecksum(key);
|
|
11346
|
+
} catch (error) {
|
|
11347
|
+
logger2?.debug("Anchored-text consult failed \u2014 treating as miss", {
|
|
11348
|
+
key,
|
|
11349
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
11350
|
+
});
|
|
11351
|
+
return null;
|
|
11352
|
+
}
|
|
11353
|
+
},
|
|
11354
|
+
async write(key) {
|
|
11355
|
+
logger2?.debug("Anchored-text write skipped \u2014 the Smelter is the sole writer", { key });
|
|
11356
|
+
},
|
|
11357
|
+
async list() {
|
|
11358
|
+
return [];
|
|
11359
|
+
}
|
|
11360
|
+
};
|
|
11361
|
+
}
|
|
11362
|
+
|
|
11331
11363
|
// src/worker-runtime.ts
|
|
11332
11364
|
var import_rxjs2 = __toESM(require_cjs());
|
|
11333
11365
|
function buildHealthPayload(workers) {
|
|
@@ -11367,7 +11399,7 @@ function startStallWatchdog(opts) {
|
|
|
11367
11399
|
timer.unref?.();
|
|
11368
11400
|
return { dispose: () => clearInterval(timer) };
|
|
11369
11401
|
}
|
|
11370
|
-
function
|
|
11402
|
+
function parseGatewayUrl(url) {
|
|
11371
11403
|
const parsed = new URL(url);
|
|
11372
11404
|
const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
|
|
11373
11405
|
const host = parsed.hostname;
|
|
@@ -11375,13 +11407,13 @@ function parseBackendUrl(url) {
|
|
|
11375
11407
|
return { protocol, host, port };
|
|
11376
11408
|
}
|
|
11377
11409
|
async function authenticateAgent(opts) {
|
|
11378
|
-
const {
|
|
11410
|
+
const { gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, provider, model, logger: logger2, retry = STARTUP_FETCH_RETRY } = opts;
|
|
11379
11411
|
if (!workerSecret2) {
|
|
11380
11412
|
throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
|
|
11381
11413
|
}
|
|
11382
11414
|
return retryWithBackoff(
|
|
11383
11415
|
async () => {
|
|
11384
|
-
const response = await fetch(`${
|
|
11416
|
+
const response = await fetch(`${gatewayBaseUrl2}/api/tokens/agent`, {
|
|
11385
11417
|
method: "POST",
|
|
11386
11418
|
headers: { "Content-Type": "application/json" },
|
|
11387
11419
|
body: JSON.stringify({ secret: workerSecret2, provider, model })
|
|
@@ -11394,7 +11426,7 @@ async function authenticateAgent(opts) {
|
|
|
11394
11426
|
isTransientFetchError,
|
|
11395
11427
|
retry,
|
|
11396
11428
|
({ attempt, attempts, delayMs, error }) => {
|
|
11397
|
-
logger2?.warn("
|
|
11429
|
+
logger2?.warn("Gateway unreachable, retrying agent authentication", {
|
|
11398
11430
|
agent: `${provider}:${model}`,
|
|
11399
11431
|
attempt,
|
|
11400
11432
|
attempts,
|
|
@@ -11405,11 +11437,11 @@ async function authenticateAgent(opts) {
|
|
|
11405
11437
|
);
|
|
11406
11438
|
}
|
|
11407
11439
|
async function startAgentWorker(opts) {
|
|
11408
|
-
const { group,
|
|
11440
|
+
const { group, gatewayBaseUrl: gatewayBaseUrl2, workerSecret: workerSecret2, contentReads: contentReads2, logger: logger2 } = opts;
|
|
11409
11441
|
const { inference } = group;
|
|
11410
|
-
const { protocol, host, port } =
|
|
11442
|
+
const { protocol, host, port } = parseGatewayUrl(gatewayBaseUrl2);
|
|
11411
11443
|
const { token: initialToken, did } = await authenticateAgent({
|
|
11412
|
-
|
|
11444
|
+
gatewayBaseUrl: gatewayBaseUrl2,
|
|
11413
11445
|
workerSecret: workerSecret2,
|
|
11414
11446
|
provider: inference.type,
|
|
11415
11447
|
model: inference.model,
|
|
@@ -11429,7 +11461,7 @@ async function startAgentWorker(opts) {
|
|
|
11429
11461
|
const token$ = new import_rxjs2.BehaviorSubject(null);
|
|
11430
11462
|
let session;
|
|
11431
11463
|
const transport = new HttpTransport({
|
|
11432
|
-
baseUrl: baseUrl(
|
|
11464
|
+
baseUrl: baseUrl(kbGatewayUrl(endpoint)),
|
|
11433
11465
|
token$,
|
|
11434
11466
|
tokenRefresher: () => session.refresh().then((t) => t ?? null)
|
|
11435
11467
|
});
|
|
@@ -11443,7 +11475,7 @@ async function startAgentWorker(opts) {
|
|
|
11443
11475
|
refresh: async () => {
|
|
11444
11476
|
try {
|
|
11445
11477
|
const { token } = await authenticateAgent({
|
|
11446
|
-
|
|
11478
|
+
gatewayBaseUrl: gatewayBaseUrl2,
|
|
11447
11479
|
workerSecret: workerSecret2,
|
|
11448
11480
|
provider: inference.type,
|
|
11449
11481
|
model: inference.model,
|
|
@@ -11468,10 +11500,12 @@ async function startAgentWorker(opts) {
|
|
|
11468
11500
|
jobTypes: group.jobTypes,
|
|
11469
11501
|
inferenceClient: group.client,
|
|
11470
11502
|
generator,
|
|
11471
|
-
// The extraction seam's cache, over
|
|
11472
|
-
// (
|
|
11473
|
-
//
|
|
11474
|
-
|
|
11503
|
+
// The extraction seam's cache, consulted over the bus and never written
|
|
11504
|
+
// (ANCHORED-TEXT-TO-SMELTER D2): the Smelter owns this store, the
|
|
11505
|
+
// Archivist answers the checksum-addressed read, and a worker that
|
|
11506
|
+
// misses extracts locally and discards.
|
|
11507
|
+
anchoredTextStore: anchoredTextOverBus(client, logger2),
|
|
11508
|
+
contentReads: contentReads2,
|
|
11475
11509
|
logger: logger2
|
|
11476
11510
|
});
|
|
11477
11511
|
logger2.info("Agent ready", {
|
|
@@ -11527,12 +11561,13 @@ function resolveWorker(jobType) {
|
|
|
11527
11561
|
`No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
|
|
11528
11562
|
);
|
|
11529
11563
|
}
|
|
11530
|
-
var
|
|
11531
|
-
if (!
|
|
11532
|
-
throw new Error("services.
|
|
11564
|
+
var gatewayPublicURL = envConfig.services?.gateway?.publicURL;
|
|
11565
|
+
if (!gatewayPublicURL) {
|
|
11566
|
+
throw new Error("services.gateway.publicURL is required in ~/.semiontconfig");
|
|
11533
11567
|
}
|
|
11534
|
-
var
|
|
11568
|
+
var gatewayBaseUrl = gatewayPublicURL;
|
|
11535
11569
|
var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
|
|
11570
|
+
var contentReads = archivistContentReads(envConfig);
|
|
11536
11571
|
var healthPort = 9090;
|
|
11537
11572
|
var logger = createProcessLogger("worker");
|
|
11538
11573
|
function clientKey(w) {
|
|
@@ -11566,7 +11601,7 @@ async function main() {
|
|
|
11566
11601
|
const { initObservabilityNode } = await import('@semiont/observability/node');
|
|
11567
11602
|
initObservabilityNode({ serviceName: "semiont-worker" });
|
|
11568
11603
|
logger.info("Starting agents", {
|
|
11569
|
-
baseUrl:
|
|
11604
|
+
baseUrl: gatewayBaseUrl,
|
|
11570
11605
|
agents: Array.from(groups.values()).map((g) => ({
|
|
11571
11606
|
provider: g.inference.type,
|
|
11572
11607
|
model: g.inference.model,
|
|
@@ -11575,7 +11610,7 @@ async function main() {
|
|
|
11575
11610
|
});
|
|
11576
11611
|
const workers = await Promise.all(
|
|
11577
11612
|
Array.from(groups.values()).map(
|
|
11578
|
-
(group) => startAgentWorker({ group,
|
|
11613
|
+
(group) => startAgentWorker({ group, gatewayBaseUrl, workerSecret, contentReads, logger })
|
|
11579
11614
|
)
|
|
11580
11615
|
);
|
|
11581
11616
|
const health = createServer((req, res) => {
|