@semiont/jobs 0.5.3 → 0.5.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.
Files changed (35) hide show
  1. package/dist/fs-job-queue.d.ts +79 -0
  2. package/dist/fs-job-queue.d.ts.map +1 -0
  3. package/dist/index.d.ts +20 -632
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +65 -56
  6. package/dist/index.js.map +1 -1
  7. package/dist/job-claim-adapter.d.ts +76 -0
  8. package/dist/job-claim-adapter.d.ts.map +1 -0
  9. package/dist/job-queue-interface.d.ts +19 -0
  10. package/dist/job-queue-interface.d.ts.map +1 -0
  11. package/dist/job-queue-state-unit.d.ts +26 -0
  12. package/dist/job-queue-state-unit.d.ts.map +1 -0
  13. package/dist/job-worker.d.ts +67 -0
  14. package/dist/job-worker.d.ts.map +1 -0
  15. package/dist/processors.d.ts +41 -0
  16. package/dist/processors.d.ts.map +1 -0
  17. package/dist/types.d.ts +319 -0
  18. package/dist/types.d.ts.map +1 -0
  19. package/dist/worker-main.d.ts +22 -2
  20. package/dist/worker-main.d.ts.map +1 -0
  21. package/dist/worker-main.js +165 -114
  22. package/dist/worker-main.js.map +1 -1
  23. package/dist/worker-process.d.ts +47 -0
  24. package/dist/worker-process.d.ts.map +1 -0
  25. package/dist/workers/annotation-detection.d.ts +61 -0
  26. package/dist/workers/annotation-detection.d.ts.map +1 -0
  27. package/dist/workers/detection/entity-extractor.d.ts +42 -0
  28. package/dist/workers/detection/entity-extractor.d.ts.map +1 -0
  29. package/dist/workers/detection/motivation-parsers.d.ts +116 -0
  30. package/dist/workers/detection/motivation-parsers.d.ts.map +1 -0
  31. package/dist/workers/detection/motivation-prompts.d.ts +57 -0
  32. package/dist/workers/detection/motivation-prompts.d.ts.map +1 -0
  33. package/dist/workers/generation/resource-generation.d.ts +23 -0
  34. package/dist/workers/generation/resource-generation.d.ts.map +1 -0
  35. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import { createTomlConfigLoader, baseUrl, RESOURCE_BROADCAST_TYPES, resourceId, validateAndCorrectOffsets, didToAgent, getLocaleEnglishName } from '@semiont/core';
1
+ import { createTomlConfigLoader, softwareToAgent, baseUrl, RESOURCE_BROADCAST_TYPES, resourceId, validateAndCorrectOffsets, didToAgent, getLocaleEnglishName } 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';
@@ -6388,9 +6388,9 @@ var require_groupBy = __commonJS({
6388
6388
  } else {
6389
6389
  duration = elementOrOptions.duration, element = elementOrOptions.element, connector = elementOrOptions.connector;
6390
6390
  }
6391
- var groups = /* @__PURE__ */ new Map();
6391
+ var groups2 = /* @__PURE__ */ new Map();
6392
6392
  var notify = function(cb) {
6393
- groups.forEach(cb);
6393
+ groups2.forEach(cb);
6394
6394
  cb(subscriber);
6395
6395
  };
6396
6396
  var handleError = function(err) {
@@ -6403,9 +6403,9 @@ var require_groupBy = __commonJS({
6403
6403
  var groupBySourceSubscriber = new OperatorSubscriber_1.OperatorSubscriber(subscriber, function(value) {
6404
6404
  try {
6405
6405
  var key_1 = keySelector(value);
6406
- var group_1 = groups.get(key_1);
6406
+ var group_1 = groups2.get(key_1);
6407
6407
  if (!group_1) {
6408
- groups.set(key_1, group_1 = connector ? connector() : new Subject_1.Subject());
6408
+ groups2.set(key_1, group_1 = connector ? connector() : new Subject_1.Subject());
6409
6409
  var grouped = createGroupedObservable(key_1, group_1);
6410
6410
  subscriber.next(grouped);
6411
6411
  if (duration) {
@@ -6413,7 +6413,7 @@ var require_groupBy = __commonJS({
6413
6413
  group_1.complete();
6414
6414
  durationSubscriber_1 === null || durationSubscriber_1 === void 0 ? void 0 : durationSubscriber_1.unsubscribe();
6415
6415
  }, void 0, void 0, function() {
6416
- return groups.delete(key_1);
6416
+ return groups2.delete(key_1);
6417
6417
  });
6418
6418
  groupBySourceSubscriber.add(innerFrom_1.innerFrom(duration(grouped)).subscribe(durationSubscriber_1));
6419
6419
  }
@@ -6427,7 +6427,7 @@ var require_groupBy = __commonJS({
6427
6427
  return consumer.complete();
6428
6428
  });
6429
6429
  }, handleError, function() {
6430
- return groups.clear();
6430
+ return groups2.clear();
6431
6431
  }, function() {
6432
6432
  teardownAttempted = true;
6433
6433
  return activeGroups === 0;
@@ -9831,7 +9831,7 @@ var AnnotationDetection = class {
9831
9831
  */
9832
9832
  static async detectComments(content, client, instructions, tone, density, language, sourceLanguage) {
9833
9833
  const prompt = MotivationPrompts.buildCommentPrompt(content, instructions, tone, density, language, sourceLanguage);
9834
- const response = await client.generateText(prompt, 3e3, 0.4);
9834
+ const response = await client.generateText(prompt, 3e3, 0.4, { format: "json" });
9835
9835
  return MotivationParsers.parseComments(response, content);
9836
9836
  }
9837
9837
  /**
@@ -9843,7 +9843,7 @@ var AnnotationDetection = class {
9843
9843
  */
9844
9844
  static async detectHighlights(content, client, instructions, density, sourceLanguage) {
9845
9845
  const prompt = MotivationPrompts.buildHighlightPrompt(content, instructions, density, sourceLanguage);
9846
- const response = await client.generateText(prompt, 2e3, 0.3);
9846
+ const response = await client.generateText(prompt, 2e3, 0.3, { format: "json" });
9847
9847
  return MotivationParsers.parseHighlights(response, content);
9848
9848
  }
9849
9849
  /**
@@ -9855,7 +9855,7 @@ var AnnotationDetection = class {
9855
9855
  */
9856
9856
  static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage) {
9857
9857
  const prompt = MotivationPrompts.buildAssessmentPrompt(content, instructions, tone, density, language, sourceLanguage);
9858
- const response = await client.generateText(prompt, 3e3, 0.3);
9858
+ const response = await client.generateText(prompt, 3e3, 0.3, { format: "json" });
9859
9859
  return MotivationParsers.parseAssessments(response, content);
9860
9860
  }
9861
9861
  /**
@@ -9885,12 +9885,12 @@ var AnnotationDetection = class {
9885
9885
  categoryInfo.examples,
9886
9886
  sourceLanguage
9887
9887
  );
9888
- const response = await client.generateText(prompt, 4e3, 0.2);
9888
+ const response = await client.generateText(prompt, 4e3, 0.2, { format: "json" });
9889
9889
  const parsedTags = MotivationParsers.parseTags(response);
9890
9890
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
9891
9891
  }
9892
9892
  };
9893
- async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences = false, logger2, sourceLanguage) {
9893
+ async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger2, sourceLanguage) {
9894
9894
  const entityTypesDescription = entityTypes.map((et) => {
9895
9895
  if (typeof et === "string") {
9896
9896
  return et;
@@ -9941,48 +9941,57 @@ If no entities are found, respond with an empty array [].
9941
9941
 
9942
9942
  Example output:
9943
9943
  [{"exact":"Alice","entityType":"Person","startOffset":0,"endOffset":5,"prefix":"","suffix":" went to"},{"exact":"Paris","entityType":"Location","startOffset":20,"endOffset":25,"prefix":"went to ","suffix":" yesterday"}]`;
9944
+ logger2.debug("Sending entity extraction request", { entityTypes: entityTypesDescription });
9944
9945
  const response = await client.generateTextWithMetadata(
9945
9946
  prompt,
9946
9947
  4e3,
9947
9948
  // Increased to handle many entities without truncation
9948
- 0.3
9949
+ 0.3,
9949
9950
  // Lower temperature for more consistent extraction
9951
+ // Force grammar-constrained JSON output. Without this, Ollama models
9952
+ // periodically emit malformed JSON (truncated brackets, mid-token
9953
+ // breaks at higher token counts) which silently parse-fails into
9954
+ // [] downstream. The prompt's schema (which keys, what types) still
9955
+ // governs *what* the JSON contains; `format: 'json'` governs that
9956
+ // it's syntactically valid.
9957
+ { format: "json" }
9950
9958
  );
9959
+ logger2.debug("Got entity extraction response", { responseLength: response.text.length });
9951
9960
  try {
9952
9961
  let jsonStr = response.text.trim();
9953
9962
  if (jsonStr.startsWith("```")) {
9954
9963
  jsonStr = jsonStr.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
9955
9964
  }
9956
9965
  const entities = JSON.parse(jsonStr);
9957
- logger2?.debug("Parsed entities from AI response", { count: entities.length });
9966
+ logger2.debug("Parsed entities from AI response", { count: entities.length });
9958
9967
  if (response.stopReason === "max_tokens") {
9959
9968
  const errorMsg = `AI response truncated: Found ${entities.length} entities but response hit max_tokens limit. Increase max_tokens or reduce resource size.`;
9960
- logger2?.error(errorMsg);
9969
+ logger2.error(errorMsg);
9961
9970
  throw new Error(errorMsg);
9962
9971
  }
9963
9972
  return entities.map((entity, idx) => {
9964
- let startOffset = entity.startOffset;
9965
- let endOffset = entity.endOffset;
9966
- logger2?.debug("Processing entity", {
9973
+ let start = entity.startOffset;
9974
+ let end = entity.endOffset;
9975
+ logger2.debug("Processing entity", {
9967
9976
  index: idx + 1,
9968
9977
  total: entities.length,
9969
9978
  type: entity.entityType,
9970
9979
  text: entity.exact,
9971
- offsetsFromAI: `[${startOffset}:${endOffset}]`
9980
+ offsetsFromAI: `[${start}:${end}]`
9972
9981
  });
9973
- const extractedText = exact.substring(startOffset, endOffset);
9982
+ const extractedText = exact.substring(start, end);
9974
9983
  let anchorMethod;
9975
9984
  if (extractedText === entity.exact) {
9976
9985
  anchorMethod = "llm-exact";
9977
- logger2?.debug("Entity anchored", {
9986
+ logger2.debug("Entity anchored", {
9978
9987
  text: entity.exact,
9979
9988
  entityType: entity.entityType,
9980
9989
  anchorMethod
9981
9990
  });
9982
9991
  } else {
9983
- logger2?.debug("LLM offsets mismatch \u2014 attempting re-anchor", {
9992
+ logger2.debug("LLM offsets mismatch \u2014 attempting re-anchor", {
9984
9993
  expected: entity.exact,
9985
- llmOffsets: `[${startOffset}:${endOffset}]`,
9994
+ llmOffsets: `[${start}:${end}]`,
9986
9995
  foundAtLlmOffsets: extractedText
9987
9996
  });
9988
9997
  let occurrenceCount = 0;
@@ -9995,10 +10004,10 @@ Example output:
9995
10004
  }
9996
10005
  if (occurrenceCount === 0) {
9997
10006
  anchorMethod = "dropped";
9998
- logger2?.error("Entity text not found in resource \u2014 dropping", {
10007
+ logger2.error("Entity text not found in resource \u2014 dropping", {
9999
10008
  text: entity.exact,
10000
10009
  entityType: entity.entityType,
10001
- llmOffsets: `[${startOffset}:${endOffset}]`,
10010
+ llmOffsets: `[${start}:${end}]`,
10002
10011
  anchorMethod,
10003
10012
  resourceStart: exact.substring(0, 200)
10004
10013
  });
@@ -10024,9 +10033,9 @@ Example output:
10024
10033
  }
10025
10034
  if (recoveredOffset !== -1) {
10026
10035
  anchorMethod = "context-recovered";
10027
- startOffset = recoveredOffset;
10028
- endOffset = recoveredOffset + entity.exact.length;
10029
- logger2?.debug("Entity anchored", {
10036
+ start = recoveredOffset;
10037
+ end = recoveredOffset + entity.exact.length;
10038
+ logger2.debug("Entity anchored", {
10030
10039
  text: entity.exact,
10031
10040
  entityType: entity.entityType,
10032
10041
  anchorMethod,
@@ -10034,9 +10043,9 @@ Example output:
10034
10043
  });
10035
10044
  } else if (occurrenceCount === 1) {
10036
10045
  anchorMethod = "unique-match";
10037
- startOffset = firstOccurrence;
10038
- endOffset = firstOccurrence + entity.exact.length;
10039
- logger2?.debug("Entity anchored", {
10046
+ start = firstOccurrence;
10047
+ end = firstOccurrence + entity.exact.length;
10048
+ logger2.debug("Entity anchored", {
10040
10049
  text: entity.exact,
10041
10050
  entityType: entity.entityType,
10042
10051
  anchorMethod,
@@ -10044,9 +10053,9 @@ Example output:
10044
10053
  });
10045
10054
  } else {
10046
10055
  anchorMethod = "first-of-many";
10047
- startOffset = firstOccurrence;
10048
- endOffset = firstOccurrence + entity.exact.length;
10049
- logger2?.warn("Entity anchored at first of multiple occurrences \u2014 may be wrong", {
10056
+ start = firstOccurrence;
10057
+ end = firstOccurrence + entity.exact.length;
10058
+ logger2.warn("Entity anchored at first of multiple occurrences \u2014 may be wrong", {
10050
10059
  text: entity.exact,
10051
10060
  entityType: entity.entityType,
10052
10061
  anchorMethod,
@@ -10061,58 +10070,71 @@ Example output:
10061
10070
  return {
10062
10071
  exact: entity.exact,
10063
10072
  entityType: entity.entityType,
10064
- startOffset,
10065
- endOffset,
10073
+ start,
10074
+ end,
10066
10075
  prefix: entity.prefix,
10067
10076
  suffix: entity.suffix
10068
10077
  };
10069
10078
  }).filter((entity) => {
10070
10079
  if (entity === null) {
10071
- logger2?.debug("Filtered entity: null");
10080
+ logger2.debug("Filtered entity: null");
10072
10081
  return false;
10073
10082
  }
10074
- if (entity.startOffset === void 0 || entity.endOffset === void 0) {
10075
- logger2?.warn("Filtered entity: missing offsets", { text: entity.exact });
10083
+ if (entity.start === void 0 || entity.end === void 0) {
10084
+ logger2.warn("Filtered entity: missing offsets", { text: entity.exact });
10076
10085
  return false;
10077
10086
  }
10078
- if (entity.startOffset < 0) {
10079
- logger2?.warn("Filtered entity: negative startOffset", {
10087
+ if (entity.start < 0) {
10088
+ logger2.warn("Filtered entity: negative start", {
10080
10089
  text: entity.exact,
10081
- startOffset: entity.startOffset
10090
+ start: entity.start
10082
10091
  });
10083
10092
  return false;
10084
10093
  }
10085
- if (entity.endOffset > exact.length) {
10086
- logger2?.warn("Filtered entity: endOffset exceeds text length", {
10094
+ if (entity.end > exact.length) {
10095
+ logger2.warn("Filtered entity: end exceeds text length", {
10087
10096
  text: entity.exact,
10088
- endOffset: entity.endOffset,
10097
+ end: entity.end,
10089
10098
  textLength: exact.length
10090
10099
  });
10091
10100
  return false;
10092
10101
  }
10093
- const extractedText = exact.substring(entity.startOffset, entity.endOffset);
10102
+ const extractedText = exact.substring(entity.start, entity.end);
10094
10103
  if (extractedText !== entity.exact) {
10095
- logger2?.warn("Filtered entity: offset mismatch", {
10104
+ logger2.warn("Filtered entity: offset mismatch", {
10096
10105
  expected: entity.exact,
10097
10106
  got: extractedText,
10098
- offsets: `[${entity.startOffset}:${entity.endOffset}]`
10107
+ offsets: `[${entity.start}:${entity.end}]`
10099
10108
  });
10100
10109
  return false;
10101
10110
  }
10102
- logger2?.debug("Accepted entity", {
10111
+ logger2.debug("Accepted entity", {
10103
10112
  text: entity.exact,
10104
- offsets: `[${entity.startOffset}:${entity.endOffset}]`
10113
+ offsets: `[${entity.start}:${entity.end}]`
10105
10114
  });
10106
10115
  return true;
10107
10116
  });
10108
10117
  } catch (error) {
10118
+ logger2.error("Failed to parse entity extraction response", {
10119
+ error: error instanceof Error ? error.message : String(error)
10120
+ });
10109
10121
  return [];
10110
10122
  }
10111
10123
  }
10112
10124
  function getLanguageName(locale) {
10113
10125
  return getLocaleEnglishName(locale) || locale;
10114
10126
  }
10115
- async function generateResourceFromTopic(topic, entityTypes, client, userPrompt, locale, context, temperature, maxTokens, logger2, sourceLanguage) {
10127
+ async function generateResourceFromTopic(topic, entityTypes, client, logger2, userPrompt, locale, context, temperature, maxTokens, sourceLanguage) {
10128
+ logger2.debug("Generating resource from topic", {
10129
+ topicPreview: topic.substring(0, 100),
10130
+ entityTypes,
10131
+ hasUserPrompt: !!userPrompt,
10132
+ locale,
10133
+ sourceLanguage,
10134
+ hasContext: !!context,
10135
+ temperature,
10136
+ maxTokens
10137
+ });
10116
10138
  const finalTemperature = temperature ?? 0.7;
10117
10139
  const finalMaxTokens = maxTokens ?? 500;
10118
10140
  const languageInstruction = locale && locale !== "en" ? `
@@ -10211,18 +10233,33 @@ Requirements:
10211
10233
  content
10212
10234
  };
10213
10235
  };
10236
+ logger2.debug("Sending prompt to inference", {
10237
+ promptLength: prompt.length,
10238
+ temperature: finalTemperature,
10239
+ maxTokens: finalMaxTokens
10240
+ });
10214
10241
  const response = await client.generateText(prompt, finalMaxTokens, finalTemperature);
10242
+ logger2.debug("Got response from inference", { responseLength: response.length });
10215
10243
  const result = parseResponse(response);
10244
+ logger2.debug("Parsed response", {
10245
+ hasTitle: !!result.title,
10246
+ titleLength: result.title?.length,
10247
+ hasContent: !!result.content,
10248
+ contentLength: result.content?.length
10249
+ });
10216
10250
  return result;
10217
10251
  }
10218
10252
  function buildTextAnnotation(resourceId, userId, generator, motivation, match, body) {
10253
+ const creator = didToAgent(userId);
10254
+ const wasAttributedTo = creator["@id"] === generator["@id"] ? [generator] : [creator, generator];
10219
10255
  return {
10220
10256
  "@context": "http://www.w3.org/ns/anno.jsonld",
10221
10257
  "type": "Annotation",
10222
10258
  "id": generateAnnotationId(),
10223
10259
  motivation,
10224
- creator: didToAgent(userId),
10260
+ creator,
10225
10261
  generator,
10262
+ wasAttributedTo,
10226
10263
  created: (/* @__PURE__ */ new Date()).toISOString(),
10227
10264
  target: {
10228
10265
  type: "SpecificResource",
@@ -10365,7 +10402,7 @@ async function processReferenceJob(content, inferenceClient, params, userId, gen
10365
10402
  ];
10366
10403
  for (const entity of extractedEntities) {
10367
10404
  try {
10368
- const validated = validateAndCorrectOffsets(content, entity.startOffset, entity.endOffset, entity.exact);
10405
+ const validated = validateAndCorrectOffsets(content, entity.start, entity.end, entity.exact);
10369
10406
  const ann = buildTextAnnotation(
10370
10407
  params.resourceId,
10371
10408
  userId,
@@ -10419,7 +10456,7 @@ async function processTagJob(content, inferenceClient, params, userId, generator
10419
10456
  result: { tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
10420
10457
  };
10421
10458
  }
10422
- async function processGenerationJob(inferenceClient, params, onProgress) {
10459
+ async function processGenerationJob(inferenceClient, params, onProgress, logger2) {
10423
10460
  onProgress(20, "Fetching context...", "fetching");
10424
10461
  const title = params.title ?? "Untitled";
10425
10462
  const entityTypes = (params.entityTypes ?? []).map(String);
@@ -10428,13 +10465,12 @@ async function processGenerationJob(inferenceClient, params, onProgress) {
10428
10465
  title,
10429
10466
  entityTypes,
10430
10467
  inferenceClient,
10468
+ logger2,
10431
10469
  params.prompt,
10432
10470
  params.language,
10433
10471
  params.context,
10434
10472
  params.temperature,
10435
10473
  params.maxTokens,
10436
- void 0,
10437
- // logger
10438
10474
  params.sourceLanguage
10439
10475
  );
10440
10476
  onProgress(85, "Creating resource...", "creating");
@@ -10509,7 +10545,7 @@ async function handleJob(adapter, config, job) {
10509
10545
  }
10510
10546
  }
10511
10547
  async function handleJobInner(adapter, config, job) {
10512
- const { session } = config;
10548
+ const { session, inferenceClient, generator } = config;
10513
10549
  const { resourceId, userId, jobId, type: jobType } = job;
10514
10550
  const annotationId = job.params.referenceId;
10515
10551
  const lifecycleBase = {
@@ -10520,12 +10556,10 @@ async function handleJobInner(adapter, config, job) {
10520
10556
  ...annotationId ? { annotationId } : {}
10521
10557
  };
10522
10558
  await emitEvent(session, "job:start", lifecycleBase);
10523
- const engine = config.engines[jobType];
10524
- if (!engine) {
10525
- adapter.failJob(jobId, `No inference engine configured for job type: ${jobType}`);
10559
+ if (!config.jobTypes.includes(jobType)) {
10560
+ adapter.failJob(jobId, `Worker not configured for job type: ${jobType}`);
10526
10561
  return;
10527
10562
  }
10528
- const { inferenceClient, generator } = engine;
10529
10563
  const onProgress = (percentage, message, stage, extra) => {
10530
10564
  emitEvent(session, "job:report-progress", {
10531
10565
  ...lifecycleBase,
@@ -10605,7 +10639,8 @@ async function handleJobInner(adapter, config, job) {
10605
10639
  job.params,
10606
10640
  userId,
10607
10641
  generator,
10608
- onProgress
10642
+ onProgress,
10643
+ config.logger
10609
10644
  );
10610
10645
  for (const ann of annotations) {
10611
10646
  await emitEvent(session, "mark:create", { annotation: ann, userId, resourceId });
@@ -10637,7 +10672,8 @@ async function handleJobInner(adapter, config, job) {
10637
10672
  const genResult = await processGenerationJob(
10638
10673
  inferenceClient,
10639
10674
  job.params,
10640
- onProgress
10675
+ onProgress,
10676
+ config.logger
10641
10677
  );
10642
10678
  const genParams = job.params;
10643
10679
  const storageUri = deriveStorageUri(genResult.title, genResult.format);
@@ -10646,7 +10682,6 @@ async function handleJobInner(adapter, config, job) {
10646
10682
  file: Buffer.from(genResult.content),
10647
10683
  format: genResult.format,
10648
10684
  storageUri,
10649
- creationMethod: "generated",
10650
10685
  sourceResourceId: resourceId,
10651
10686
  ...genParams.referenceId ? { sourceAnnotationId: genParams.referenceId } : {},
10652
10687
  ...genParams.prompt ? { generationPrompt: genParams.prompt } : {},
@@ -10718,24 +10753,20 @@ function toClientConfig(w) {
10718
10753
  ...w.apiKey && { apiKey: w.apiKey }
10719
10754
  };
10720
10755
  }
10721
- var clientCache = /* @__PURE__ */ new Map();
10722
- var engines = {};
10756
+ var groups = /* @__PURE__ */ new Map();
10723
10757
  for (const jobType of ALL_JOB_TYPES) {
10724
- const w = resolveWorker(jobType);
10725
- const key = clientKey(w);
10726
- let client = clientCache.get(key);
10727
- if (!client) {
10728
- client = createInferenceClient(toClientConfig(w), logger);
10729
- clientCache.set(key, client);
10730
- }
10731
- const generator = {
10732
- "@type": "SoftwareAgent",
10733
- name: `worker-pool / ${w.type} ${w.model}`,
10734
- worker: "worker-pool",
10735
- inferenceProvider: w.type,
10736
- model: w.model
10737
- };
10738
- engines[jobType] = { inferenceClient: client, generator };
10758
+ const inference = resolveWorker(jobType);
10759
+ const key = clientKey(inference);
10760
+ let group = groups.get(key);
10761
+ if (!group) {
10762
+ group = {
10763
+ inference,
10764
+ jobTypes: [],
10765
+ client: createInferenceClient(toClientConfig(inference), logger)
10766
+ };
10767
+ groups.set(key, group);
10768
+ }
10769
+ group.jobTypes.push(jobType);
10739
10770
  }
10740
10771
  function parseBackendUrl(url) {
10741
10772
  const parsed = new URL(url);
@@ -10744,35 +10775,35 @@ function parseBackendUrl(url) {
10744
10775
  const port = parsed.port ? Number(parsed.port) : protocol === "https" ? 443 : 80;
10745
10776
  return { protocol, host, port };
10746
10777
  }
10747
- async function authenticate() {
10778
+ async function authenticateAgent(provider, model) {
10748
10779
  if (!workerSecret) {
10749
- logger.warn("No SEMIONT_WORKER_SECRET set \u2014 using empty token");
10750
- return "";
10780
+ throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
10751
10781
  }
10752
- const response = await fetch(`${backendBaseUrl}/api/tokens/worker`, {
10782
+ const response = await fetch(`${backendBaseUrl}/api/tokens/agent`, {
10753
10783
  method: "POST",
10754
10784
  headers: { "Content-Type": "application/json" },
10755
- body: JSON.stringify({ secret: workerSecret })
10785
+ body: JSON.stringify({ secret: workerSecret, provider, model })
10756
10786
  });
10757
10787
  if (!response.ok) {
10758
- throw new Error(`Authentication failed: ${response.status} ${response.statusText}`);
10788
+ throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
10759
10789
  }
10760
- const { token } = await response.json();
10761
- return token;
10790
+ return await response.json();
10762
10791
  }
10763
- async function main() {
10764
- const { initObservabilityNode } = await import('@semiont/observability/node');
10765
- initObservabilityNode({ serviceName: "semiont-worker" });
10766
- logger.info("Authenticating", { baseUrl: backendBaseUrl });
10767
- const initialToken = await authenticate();
10768
- logger.info("Authenticated");
10792
+ async function startAgentWorker(group) {
10793
+ const { inference } = group;
10769
10794
  const { protocol, host, port } = parseBackendUrl(backendBaseUrl);
10770
- const kbId = `worker-${hostname()}`;
10795
+ const { token: initialToken, did } = await authenticateAgent(inference.type, inference.model);
10796
+ const generator = softwareToAgent({
10797
+ domain: host,
10798
+ provider: inference.type,
10799
+ model: inference.model
10800
+ });
10801
+ const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
10771
10802
  const endpoint = { kind: "http", host, port, protocol };
10772
10803
  const kb = {
10773
10804
  id: kbId,
10774
- label: `Worker pool @ ${host}`,
10775
- email: `worker-pool@${host}`,
10805
+ label: `${inference.type} / ${inference.model} @ ${host}`,
10806
+ email: `agent@${host}`,
10776
10807
  endpoint
10777
10808
  };
10778
10809
  const storage = new InMemorySessionStorage();
@@ -10793,37 +10824,58 @@ async function main() {
10793
10824
  token$,
10794
10825
  refresh: async () => {
10795
10826
  try {
10796
- return await authenticate();
10827
+ const { token } = await authenticateAgent(inference.type, inference.model);
10828
+ return token;
10797
10829
  } catch (err) {
10798
- logger.error("Worker token refresh failed", {
10799
- error: err instanceof Error ? err.message : String(err)
10830
+ logger.error("Agent token refresh failed", {
10831
+ error: err instanceof Error ? err.message : String(err),
10832
+ agent: did
10800
10833
  });
10801
10834
  return null;
10802
10835
  }
10803
10836
  },
10804
- // No validate callback — workers are service principals with no
10805
- // user record to fetch. `session.user$` stays null.
10806
10837
  onError: (err) => {
10807
- logger.error("Session error", { code: err.code, message: err.message });
10838
+ logger.error("Session error", { code: err.code, message: err.message, agent: did });
10808
10839
  }
10809
10840
  });
10810
10841
  await session.ready;
10811
- const workerVm = startWorkerProcess({
10842
+ const adapter = startWorkerProcess({
10812
10843
  session,
10813
- jobTypes: ALL_JOB_TYPES,
10814
- engines,
10844
+ jobTypes: group.jobTypes,
10845
+ inferenceClient: group.client,
10846
+ generator,
10815
10847
  logger
10816
10848
  });
10817
- logger.info("Connected", {
10849
+ logger.info("Agent ready", {
10850
+ did,
10851
+ provider: inference.type,
10852
+ model: inference.model,
10853
+ jobTypes: group.jobTypes
10854
+ });
10855
+ return {
10856
+ session,
10857
+ dispose: async () => {
10858
+ adapter.dispose();
10859
+ await session.dispose();
10860
+ }
10861
+ };
10862
+ }
10863
+ async function main() {
10864
+ const { initObservabilityNode } = await import('@semiont/observability/node');
10865
+ initObservabilityNode({ serviceName: "semiont-worker" });
10866
+ logger.info("Starting agents", {
10818
10867
  baseUrl: backendBaseUrl,
10819
- engines: Object.fromEntries(
10820
- Object.entries(engines).map(([jt, e]) => [jt, `${e.generator.inferenceProvider} / ${e.generator.model}`])
10821
- )
10868
+ agents: Array.from(groups.values()).map((g) => ({
10869
+ provider: g.inference.type,
10870
+ model: g.inference.model,
10871
+ jobTypes: g.jobTypes
10872
+ }))
10822
10873
  });
10874
+ const workers = await Promise.all(Array.from(groups.values()).map(startAgentWorker));
10823
10875
  const health = createServer((req, res) => {
10824
10876
  if (req.url === "/health") {
10825
10877
  res.writeHead(200, { "Content-Type": "application/json" });
10826
- res.end(JSON.stringify({ status: "ok" }));
10878
+ res.end(JSON.stringify({ status: "ok", agents: workers.length }));
10827
10879
  } else {
10828
10880
  res.writeHead(404);
10829
10881
  res.end();
@@ -10834,8 +10886,7 @@ async function main() {
10834
10886
  });
10835
10887
  const shutdown = async () => {
10836
10888
  logger.info("Shutting down");
10837
- workerVm.dispose();
10838
- await session.dispose();
10889
+ await Promise.all(workers.map((w) => w.dispose()));
10839
10890
  health.close();
10840
10891
  process.exit(0);
10841
10892
  };