@semiont/jobs 0.5.10 → 0.5.11

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.
@@ -1,14 +1,14 @@
1
- import { createTomlConfigLoader, softwareToAgent, baseUrl, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, didToAgent, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
1
+ import { createTomlConfigLoader, didToAgent, baseUrl, 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';
5
+ import { homedir, hostname } from 'os';
6
+ import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
7
+ import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
5
8
  import { createInferenceClient } from '@semiont/inference';
6
9
  import { createServer } from 'http';
7
10
  import { existsSync, readFileSync } from 'fs';
8
- import { homedir, hostname } from 'os';
9
11
  import { join } from 'path';
10
- import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
11
- import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
12
12
  import { createProcessLogger } from '@semiont/observability/process-logger';
13
13
 
14
14
  var __create = Object.create;
@@ -9940,7 +9940,13 @@ Example output:
9940
9940
  function getLanguageName(locale) {
9941
9941
  return getLocaleEnglishName(locale) || locale;
9942
9942
  }
9943
- async function generateResourceFromTopic(topic, entityTypes, client, logger2, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown") {
9943
+ var RESOURCE_CONTENT_CAP = 4e3;
9944
+ var SEMANTIC_MATCH_LIMIT = 3;
9945
+ var SEMANTIC_MATCH_CHARS = 240;
9946
+ function idLabel(resourceId, annotationId) {
9947
+ return `[${resourceId}${annotationId ? `/${annotationId}` : ""}]`;
9948
+ }
9949
+ async function generateResourceFromTopic(topic, entityTypes, client, logger2, userPrompt, locale, context, temperature, maxTokens, sourceLanguage, outputMediaType = "text/markdown", task = "resource", structure, cite = false) {
9944
9950
  logger2.debug("Generating resource from topic", {
9945
9951
  topicPreview: topic.substring(0, 100),
9946
9952
  entityTypes,
@@ -9949,7 +9955,10 @@ async function generateResourceFromTopic(topic, entityTypes, client, logger2, us
9949
9955
  sourceLanguage,
9950
9956
  hasContext: !!context,
9951
9957
  temperature,
9952
- maxTokens
9958
+ maxTokens,
9959
+ outputMediaType,
9960
+ task,
9961
+ structure
9953
9962
  });
9954
9963
  const finalTemperature = temperature ?? 0.7;
9955
9964
  const finalMaxTokens = maxTokens ?? 500;
@@ -9970,7 +9979,7 @@ The source resource and embedded context are in ${getLanguageName(sourceLanguage
9970
9979
  if (focus.kind === "annotation") {
9971
9980
  const parts = [];
9972
9981
  parts.push(`- Annotation motivation: ${focus.annotation.motivation}`);
9973
- parts.push(`- Source resource: ${focus.sourceResource.name}`);
9982
+ parts.push(`- Source resource: ${focus.sourceResource.name} ${idLabel(mainResourceId)}`);
9974
9983
  const { motivation, body } = focus.annotation;
9975
9984
  if (motivation === "commenting" || motivation === "assessing") {
9976
9985
  const bodyItem = Array.isArray(body) ? body[0] : body;
@@ -9996,8 +10005,7 @@ ${after ? `${after}...` : ""}
9996
10005
  `;
9997
10006
  }
9998
10007
  } else {
9999
- const RESOURCE_CONTENT_CAP = 4e3;
10000
- const parts = [`- Resource: ${focus.resource.name}`];
10008
+ const parts = [`- Resource: ${focus.resource.name} ${idLabel(mainResourceId)}`];
10001
10009
  if (focus.summary) parts.push(`- Summary: ${focus.summary}`);
10002
10010
  if (focus.suggestedReferences && focus.suggestedReferences.length > 0) {
10003
10011
  parts.push(`- Suggested references: ${focus.suggestedReferences.join(", ")}`);
@@ -10030,11 +10038,11 @@ ${blocks}
10030
10038
  const views = deriveViews(context.graph, mainResourceId, focalAnnotationId);
10031
10039
  const parts = [];
10032
10040
  if (views.connections.length > 0) {
10033
- const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""}`).join(", ");
10041
+ const connList = views.connections.map((c) => `${c.resourceName}${c.entityTypes.length ? ` (${c.entityTypes.join(", ")})` : ""} ${idLabel(c.resourceId)}`).join(", ");
10034
10042
  parts.push(`- Connected resources: ${connList}`);
10035
10043
  }
10036
10044
  if (views.citedByCount > 0) {
10037
- const citedNames = views.citedBy.map((c) => c.resourceName).join(", ");
10045
+ const citedNames = views.citedBy.map((c) => `${c.resourceName} ${idLabel(c.resourceId)}`).join(", ");
10038
10046
  parts.push(`- This resource is cited by ${views.citedByCount} other resource${views.citedByCount > 1 ? "s" : ""}${citedNames ? `: ${citedNames}` : ""}`);
10039
10047
  }
10040
10048
  if (views.siblingEntityTypes.length > 0) {
@@ -10054,25 +10062,52 @@ ${parts.join("\n")}`;
10054
10062
  let semanticContextSection = "";
10055
10063
  const similar = context?.semanticContext?.similar ?? [];
10056
10064
  if (similar.length > 0) {
10057
- const lines = [...similar].sort((a, b) => b.score - a.score).slice(0, 3).map((m) => `- (${m.score.toFixed(2)}) ${m.text.slice(0, 240)}`);
10065
+ const lines = [...similar].sort((a, b) => b.score - a.score).slice(0, SEMANTIC_MATCH_LIMIT).map((m) => `- ${idLabel(m.resourceId, m.annotationId)} (${m.score.toFixed(2)}) ${m.text.slice(0, SEMANTIC_MATCH_CHARS)}`);
10058
10066
  semanticContextSection = `
10059
10067
 
10060
10068
  Related passages from the knowledge base:
10061
10069
  ${lines.join("\n")}`;
10070
+ }
10071
+ let leadLine;
10072
+ if (task === "resource") {
10073
+ leadLine = `Generate a concise, informative resource about "${topic}".`;
10074
+ } else if (task === "answer") {
10075
+ leadLine = `Answer the following question directly and concisely, grounded in the provided context: "${topic}"`;
10076
+ } else if (task === "summary") {
10077
+ leadLine = `Write a concise summary of "${topic}".`;
10078
+ } else {
10079
+ logger2.warn("Unknown task \u2014 using it verbatim as the framing instruction", { task });
10080
+ leadLine = `${task}
10081
+ Topic: "${topic}"`;
10062
10082
  }
10063
10083
  const isPlainText = outputMediaType === "text/plain";
10064
- const structureGuidance = !isPlainText && finalMaxTokens >= 1e3 ? "organized into titled sections (## Section) with well-structured paragraphs" : "organized into well-structured paragraphs";
10084
+ let structureRequirement = "";
10085
+ let titleRequirement = "";
10086
+ if (structure === "sections") {
10087
+ structureRequirement = isPlainText ? "\n- Organize the content into titled sections with well-structured paragraphs" : "\n- Organize the content into titled sections (## Section) with well-structured paragraphs";
10088
+ if (!isPlainText) {
10089
+ titleRequirement = "\n- Start with a clear heading (# Title)";
10090
+ }
10091
+ } else if (structure === "prose") {
10092
+ structureRequirement = "\n- Write flowing, well-structured paragraphs with no section headings";
10093
+ } else if (structure === "chat") {
10094
+ structureRequirement = "\n- Structure the content as a conversational chat transcript \u2014 a sequence of alternating, speaker-labeled turns (no section headings)";
10095
+ } else if (structure) {
10096
+ logger2.warn("Unknown structure \u2014 passing it through as freeform organization guidance", { structure });
10097
+ structureRequirement = `
10098
+ - Organize the output as: ${structure}`;
10099
+ }
10100
+ const citeRequirement = cite ? "\n- Ground every claim in the provided context. Immediately after each claim, cite its source by emitting [[<id>]], where <id> is an id shown in square brackets in the context above (for a passage labeled [abc], emit [[abc]]). Cite only ids that appear in the context." : "";
10065
10101
  const formatRequirements = isPlainText ? `- Write the response as plain text \u2014 no formatting markup (no #, *, backticks, headings, or links)
10066
- - Begin with the title on its own first line` : `- Start with a clear heading (# Title)
10067
- - Use markdown formatting
10102
+ - Begin with the title on its own first line` : `- Use markdown formatting
10068
10103
  - Write the response as markdown`;
10069
- const prompt = `Generate a concise, informative resource about "${topic}".
10070
- ${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}
10071
- ${userPrompt ? `Additional context: ${userPrompt}` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
10104
+ const prompt = `${leadLine}
10105
+ ${userPrompt ? `Instruction: ${userPrompt}` : ""}
10106
+ ${entityTypes.length > 0 ? `Focus on these entity types: ${entityTypes.join(", ")}.` : ""}${annotationSection}${contextSection}${resourceSection}${graphSection}${semanticContextSection}${sourceLanguageInstruction}${languageInstruction}
10072
10107
 
10073
10108
  Requirements:
10074
- - Aim for approximately ${finalMaxTokens} tokens of content, ${structureGuidance}
10075
- - Be factual and informative
10109
+ - Aim for approximately ${finalMaxTokens} tokens of content
10110
+ - Be factual and informative${structureRequirement}${titleRequirement}${citeRequirement}
10076
10111
  ${formatRequirements}`;
10077
10112
  const parseResponse = (response2) => {
10078
10113
  let content = response2.trim();
@@ -10111,6 +10146,67 @@ ${formatRequirements}`;
10111
10146
  });
10112
10147
  return result;
10113
10148
  }
10149
+
10150
+ // src/workers/generation/citation-resolver.ts
10151
+ var CITATION_TOKEN = /\[\[([^\s[\]/]+)(?:\/([^\s[\]/]+))?\]\]/g;
10152
+ function collectContextResourceIds(context) {
10153
+ const ids = /* @__PURE__ */ new Set();
10154
+ if (!context) return ids;
10155
+ const { focus } = context;
10156
+ ids.add(focus.kind === "annotation" ? focus.sourceResource["@id"] : focus.resource["@id"]);
10157
+ for (const node of context.graph.nodes) {
10158
+ if (node.type === "resource") ids.add(node.id);
10159
+ }
10160
+ for (const m of context.semanticContext?.similar ?? []) ids.add(m.resourceId);
10161
+ if (focus.kind === "resource") {
10162
+ for (const id of Object.keys(focus.content?.related ?? {})) ids.add(id);
10163
+ }
10164
+ return ids;
10165
+ }
10166
+ function resolveCitationTokens(content, validResourceIds, logger2) {
10167
+ const citations = [];
10168
+ let clean = "";
10169
+ let last = 0;
10170
+ for (const match of content.matchAll(CITATION_TOKEN)) {
10171
+ const token = match[0];
10172
+ const citedResourceId = match[1];
10173
+ const annotationId = match[2];
10174
+ clean += content.slice(last, match.index).replace(/[ \t]+$/, "");
10175
+ last = match.index + token.length;
10176
+ if (!validResourceIds.has(citedResourceId)) {
10177
+ logger2.warn("Citation token references an id absent from the provided context \u2014 dropped", {
10178
+ resourceId: citedResourceId
10179
+ });
10180
+ continue;
10181
+ }
10182
+ const end = clean.length;
10183
+ let start = 0;
10184
+ for (let i = end - 2; i >= 0; i--) {
10185
+ const ch = clean[i];
10186
+ if (ch === "." || ch === "!" || ch === "?" || ch === "\n") {
10187
+ start = i + 1;
10188
+ break;
10189
+ }
10190
+ }
10191
+ while (start < end && /\s/.test(clean[start])) start++;
10192
+ const exact = clean.slice(start, end);
10193
+ if (exact.length === 0) {
10194
+ logger2.warn("Citation token has no preceding claim text \u2014 dropped", {
10195
+ resourceId: citedResourceId
10196
+ });
10197
+ continue;
10198
+ }
10199
+ citations.push({
10200
+ resourceId: citedResourceId,
10201
+ ...annotationId ? { annotationId } : {},
10202
+ start,
10203
+ end,
10204
+ exact
10205
+ });
10206
+ }
10207
+ clean += content.slice(last);
10208
+ return { content: clean, citations };
10209
+ }
10114
10210
  function toMatch(r) {
10115
10211
  return {
10116
10212
  exact: r.exact,
@@ -10414,13 +10510,24 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
10414
10510
  params.temperature,
10415
10511
  params.maxTokens,
10416
10512
  params.sourceLanguage,
10417
- outputMediaType
10513
+ outputMediaType,
10514
+ params.task,
10515
+ params.structure,
10516
+ params.cite
10418
10517
  );
10518
+ let content = generated.content;
10519
+ let citations = [];
10520
+ if (params.cite === true) {
10521
+ const resolved = resolveCitationTokens(content, collectContextResourceIds(params.context), logger2);
10522
+ content = resolved.content;
10523
+ citations = resolved.citations;
10524
+ }
10419
10525
  onProgress(85, "Creating resource...", "creating");
10420
10526
  return {
10421
- content: generated.content,
10527
+ content,
10422
10528
  title: generated.title ?? title,
10423
10529
  format: outputMediaType,
10530
+ citations,
10424
10531
  result: {
10425
10532
  resourceId: "",
10426
10533
  resourceName: generated.title ?? title
@@ -10651,6 +10758,23 @@ async function handleJobInner(adapter, config, job) {
10651
10758
  );
10652
10759
  await emitEvent(session, "mark:create", { annotation: provenanceRef, userId, resourceId });
10653
10760
  }
10761
+ for (const citation of genResult.citations) {
10762
+ const { annotation: citationRef } = assembleAnnotation(
10763
+ {
10764
+ motivation: "linking",
10765
+ target: {
10766
+ source: String(newResourceId),
10767
+ selector: [
10768
+ { type: "TextPositionSelector", start: citation.start, end: citation.end },
10769
+ { type: "TextQuoteSelector", exact: citation.exact }
10770
+ ]
10771
+ },
10772
+ body: { type: "SpecificResource", source: citation.resourceId, purpose: "linking" }
10773
+ },
10774
+ generator
10775
+ );
10776
+ await emitEvent(session, "mark:create", { annotation: citationRef, userId, resourceId: newResourceId });
10777
+ }
10654
10778
  await emitEvent(session, "job:complete", {
10655
10779
  ...lifecycleBase,
10656
10780
  result: { resourceId: newResourceId, resourceName: genResult.title }
@@ -10661,75 +10785,8 @@ async function handleJobInner(adapter, config, job) {
10661
10785
  }
10662
10786
  }
10663
10787
 
10664
- // src/worker-main.ts
10788
+ // src/worker-runtime.ts
10665
10789
  var import_rxjs2 = __toESM(require_cjs());
10666
- var ALL_JOB_TYPES = [
10667
- "reference-annotation",
10668
- "generation",
10669
- "highlight-annotation",
10670
- "assessment-annotation",
10671
- "comment-annotation",
10672
- "tag-annotation"
10673
- ];
10674
- var configPath = join(homedir(), ".semiontconfig");
10675
- var tomlReader = {
10676
- readIfExists: (p) => existsSync(p) ? readFileSync(p, "utf-8") : null
10677
- };
10678
- var envConfig = createTomlConfigLoader(
10679
- tomlReader,
10680
- configPath,
10681
- process.env
10682
- )(null, "local");
10683
- var workerInferenceMap = envConfig._metadata?.workers;
10684
- if (!workerInferenceMap || Object.keys(workerInferenceMap).length === 0) {
10685
- throw new Error(
10686
- 'No worker inference config found in ~/.semiontconfig. Add at least [environments.<env>.workers.default.inference] with type = "..." and model = "...".'
10687
- );
10688
- }
10689
- function resolveWorker(jobType) {
10690
- const specific = workerInferenceMap[jobType];
10691
- if (specific) return specific;
10692
- const def = workerInferenceMap["default"];
10693
- if (def) return def;
10694
- throw new Error(
10695
- `No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
10696
- );
10697
- }
10698
- var backendPublicURL = envConfig.services?.backend?.publicURL;
10699
- if (!backendPublicURL) {
10700
- throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
10701
- }
10702
- var backendBaseUrl = backendPublicURL;
10703
- var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
10704
- var healthPort = 9090;
10705
- var logger = createProcessLogger("worker");
10706
- function clientKey(w) {
10707
- return [w.type, w.model, w.apiKey ?? "", w.endpoint ?? "", w.baseURL ?? ""].join("|");
10708
- }
10709
- function toClientConfig(w) {
10710
- return {
10711
- type: w.type,
10712
- model: w.model,
10713
- ...w.endpoint && { endpoint: w.endpoint },
10714
- ...w.baseURL && { baseURL: w.baseURL },
10715
- ...w.apiKey && { apiKey: w.apiKey }
10716
- };
10717
- }
10718
- var groups = /* @__PURE__ */ new Map();
10719
- for (const jobType of ALL_JOB_TYPES) {
10720
- const inference = resolveWorker(jobType);
10721
- const key = clientKey(inference);
10722
- let group = groups.get(key);
10723
- if (!group) {
10724
- group = {
10725
- inference,
10726
- jobTypes: [],
10727
- client: createInferenceClient(toClientConfig(inference), logger)
10728
- };
10729
- groups.set(key, group);
10730
- }
10731
- group.jobTypes.push(jobType);
10732
- }
10733
10790
  function parseBackendUrl(url) {
10734
10791
  const parsed = new URL(url);
10735
10792
  const protocol = parsed.protocol.replace(":", "") === "https" ? "https" : "http";
@@ -10737,29 +10794,32 @@ function parseBackendUrl(url) {
10737
10794
  const port = parsed.port ? Number(parsed.port) : protocol === "https" ? 443 : 80;
10738
10795
  return { protocol, host, port };
10739
10796
  }
10740
- async function authenticateAgent(provider, model) {
10741
- if (!workerSecret) {
10797
+ async function authenticateAgent(opts) {
10798
+ const { backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, provider, model } = opts;
10799
+ if (!workerSecret2) {
10742
10800
  throw new Error("SEMIONT_WORKER_SECRET is required to authenticate worker agents");
10743
10801
  }
10744
- const response = await fetch(`${backendBaseUrl}/api/tokens/agent`, {
10802
+ const response = await fetch(`${backendBaseUrl2}/api/tokens/agent`, {
10745
10803
  method: "POST",
10746
10804
  headers: { "Content-Type": "application/json" },
10747
- body: JSON.stringify({ secret: workerSecret, provider, model })
10805
+ body: JSON.stringify({ secret: workerSecret2, provider, model })
10748
10806
  });
10749
10807
  if (!response.ok) {
10750
10808
  throw new Error(`Agent authentication failed for ${provider}:${model}: ${response.status} ${response.statusText}`);
10751
10809
  }
10752
10810
  return await response.json();
10753
10811
  }
10754
- async function startAgentWorker(group) {
10812
+ async function startAgentWorker(opts) {
10813
+ const { group, backendBaseUrl: backendBaseUrl2, workerSecret: workerSecret2, logger: logger2 } = opts;
10755
10814
  const { inference } = group;
10756
- const { protocol, host, port } = parseBackendUrl(backendBaseUrl);
10757
- const { token: initialToken, did } = await authenticateAgent(inference.type, inference.model);
10758
- const generator = softwareToAgent({
10759
- domain: host,
10815
+ const { protocol, host, port } = parseBackendUrl(backendBaseUrl2);
10816
+ const { token: initialToken, did } = await authenticateAgent({
10817
+ backendBaseUrl: backendBaseUrl2,
10818
+ workerSecret: workerSecret2,
10760
10819
  provider: inference.type,
10761
10820
  model: inference.model
10762
10821
  });
10822
+ const generator = didToAgent(did);
10763
10823
  const kbId = `agent-${inference.type}-${inference.model}-${hostname()}`;
10764
10824
  const endpoint = { kind: "http", host, port, protocol };
10765
10825
  const kb = {
@@ -10786,10 +10846,15 @@ async function startAgentWorker(group) {
10786
10846
  token$,
10787
10847
  refresh: async () => {
10788
10848
  try {
10789
- const { token } = await authenticateAgent(inference.type, inference.model);
10849
+ const { token } = await authenticateAgent({
10850
+ backendBaseUrl: backendBaseUrl2,
10851
+ workerSecret: workerSecret2,
10852
+ provider: inference.type,
10853
+ model: inference.model
10854
+ });
10790
10855
  return token;
10791
10856
  } catch (err) {
10792
- logger.error("Agent token refresh failed", {
10857
+ logger2.error("Agent token refresh failed", {
10793
10858
  error: err instanceof Error ? err.message : String(err),
10794
10859
  agent: did
10795
10860
  });
@@ -10797,7 +10862,7 @@ async function startAgentWorker(group) {
10797
10862
  }
10798
10863
  },
10799
10864
  onError: (err) => {
10800
- logger.error("Session error", { code: err.code, message: err.message, agent: did });
10865
+ logger2.error("Session error", { code: err.code, message: err.message, agent: did });
10801
10866
  }
10802
10867
  });
10803
10868
  await session.ready;
@@ -10806,9 +10871,9 @@ async function startAgentWorker(group) {
10806
10871
  jobTypes: group.jobTypes,
10807
10872
  inferenceClient: group.client,
10808
10873
  generator,
10809
- logger
10874
+ logger: logger2
10810
10875
  });
10811
- logger.info("Agent ready", {
10876
+ logger2.info("Agent ready", {
10812
10877
  did,
10813
10878
  provider: inference.type,
10814
10879
  model: inference.model,
@@ -10822,6 +10887,73 @@ async function startAgentWorker(group) {
10822
10887
  }
10823
10888
  };
10824
10889
  }
10890
+ var ALL_JOB_TYPES = [
10891
+ "reference-annotation",
10892
+ "generation",
10893
+ "highlight-annotation",
10894
+ "assessment-annotation",
10895
+ "comment-annotation",
10896
+ "tag-annotation"
10897
+ ];
10898
+ var configPath = join(homedir(), ".semiontconfig");
10899
+ var tomlReader = {
10900
+ readIfExists: (p) => existsSync(p) ? readFileSync(p, "utf-8") : null
10901
+ };
10902
+ var envConfig = createTomlConfigLoader(
10903
+ tomlReader,
10904
+ configPath,
10905
+ process.env
10906
+ )(null, "local");
10907
+ var workerInferenceMap = envConfig._metadata?.workers;
10908
+ if (!workerInferenceMap || Object.keys(workerInferenceMap).length === 0) {
10909
+ throw new Error(
10910
+ 'No worker inference config found in ~/.semiontconfig. Add at least [environments.<env>.workers.default.inference] with type = "..." and model = "...".'
10911
+ );
10912
+ }
10913
+ function resolveWorker(jobType) {
10914
+ const specific = workerInferenceMap[jobType];
10915
+ if (specific) return specific;
10916
+ const def = workerInferenceMap["default"];
10917
+ if (def) return def;
10918
+ throw new Error(
10919
+ `No inference config for worker '${jobType}' and no workers.default in ~/.semiontconfig.`
10920
+ );
10921
+ }
10922
+ var backendPublicURL = envConfig.services?.backend?.publicURL;
10923
+ if (!backendPublicURL) {
10924
+ throw new Error("services.backend.publicURL is required in ~/.semiontconfig");
10925
+ }
10926
+ var backendBaseUrl = backendPublicURL;
10927
+ var workerSecret = process.env.SEMIONT_WORKER_SECRET ?? "";
10928
+ var healthPort = 9090;
10929
+ var logger = createProcessLogger("worker");
10930
+ function clientKey(w) {
10931
+ return [w.type, w.model, w.apiKey ?? "", w.endpoint ?? "", w.baseURL ?? ""].join("|");
10932
+ }
10933
+ function toClientConfig(w) {
10934
+ return {
10935
+ type: w.type,
10936
+ model: w.model,
10937
+ ...w.endpoint && { endpoint: w.endpoint },
10938
+ ...w.baseURL && { baseURL: w.baseURL },
10939
+ ...w.apiKey && { apiKey: w.apiKey }
10940
+ };
10941
+ }
10942
+ var groups = /* @__PURE__ */ new Map();
10943
+ for (const jobType of ALL_JOB_TYPES) {
10944
+ const inference = resolveWorker(jobType);
10945
+ const key = clientKey(inference);
10946
+ let group = groups.get(key);
10947
+ if (!group) {
10948
+ group = {
10949
+ inference,
10950
+ jobTypes: [],
10951
+ client: createInferenceClient(toClientConfig(inference), logger)
10952
+ };
10953
+ groups.set(key, group);
10954
+ }
10955
+ group.jobTypes.push(jobType);
10956
+ }
10825
10957
  async function main() {
10826
10958
  const { initObservabilityNode } = await import('@semiont/observability/node');
10827
10959
  initObservabilityNode({ serviceName: "semiont-worker" });
@@ -10833,7 +10965,11 @@ async function main() {
10833
10965
  jobTypes: g.jobTypes
10834
10966
  }))
10835
10967
  });
10836
- const workers = await Promise.all(Array.from(groups.values()).map(startAgentWorker));
10968
+ const workers = await Promise.all(
10969
+ Array.from(groups.values()).map(
10970
+ (group) => startAgentWorker({ group, backendBaseUrl, workerSecret, logger })
10971
+ )
10972
+ );
10837
10973
  const health = createServer((req, res) => {
10838
10974
  if (req.url === "/health") {
10839
10975
  res.writeHead(200, { "Content-Type": "application/json" });