@semiont/jobs 0.5.26 → 0.5.27

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 CHANGED
@@ -2,11 +2,11 @@ import { promises, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'fs';
2
2
  import * as path from 'path';
3
3
  import { join } from 'path';
4
4
  import { jobId, deriveViews, reconcileSelector, GENERATABLE_MEDIA_TYPES, estimateTokens, chunkText, isObject, isString, getLocaleEnglishName } from '@semiont/core';
5
+ import { withSpan } from '@semiont/observability';
5
6
  import { execFileSync } from 'child_process';
6
7
  import { tmpdir } from 'os';
7
8
  import { withinByteBudget, MAX_PDF_BYTES } from '@semiont/content';
8
9
  import '@semiont/event-sourcing';
9
- import '@semiont/observability';
10
10
  import '@semiont/sdk';
11
11
  import '@semiont/http-transport';
12
12
 
@@ -426,10 +426,18 @@ function isFailedJob(job) {
426
426
  function isCancelledJob(job) {
427
427
  return job.status === "cancelled";
428
428
  }
429
-
430
- // src/workers/inference-call.ts
431
429
  var INFERENCE_TIMEOUT_MS = 10 * 6e4;
432
- async function withTimeout(work, label) {
430
+ var INFERENCE_HEARTBEAT_MS = 15e3;
431
+ function spanned(client, kind, maxTokens, work) {
432
+ return withSpan(`inference:${kind}`, work, {
433
+ attrs: {
434
+ "inference.provider": client.type,
435
+ "inference.model": client.modelId,
436
+ "inference.max_tokens": maxTokens
437
+ }
438
+ });
439
+ }
440
+ async function withTimeout(work, label, onHeartbeat) {
433
441
  let timer;
434
442
  const timedOut = new Promise((_, reject) => {
435
443
  timer = setTimeout(() => {
@@ -439,6 +447,16 @@ async function withTimeout(work, label) {
439
447
  }, INFERENCE_TIMEOUT_MS);
440
448
  timer.unref?.();
441
449
  });
450
+ let heartbeat;
451
+ if (onHeartbeat) {
452
+ heartbeat = setInterval(() => {
453
+ try {
454
+ onHeartbeat();
455
+ } catch {
456
+ }
457
+ }, INFERENCE_HEARTBEAT_MS);
458
+ heartbeat.unref?.();
459
+ }
442
460
  try {
443
461
  return await Promise.race([work, timedOut]);
444
462
  } catch (err) {
@@ -447,19 +465,22 @@ async function withTimeout(work, label) {
447
465
  throw err;
448
466
  } finally {
449
467
  clearTimeout(timer);
468
+ if (heartbeat) clearInterval(heartbeat);
450
469
  }
451
470
  }
452
- function boundedGenerate(client, prompt, maxTokens, temperature) {
453
- return withTimeout(
471
+ function boundedGenerate(client, prompt, maxTokens, temperature, onHeartbeat) {
472
+ return spanned(client, "text", maxTokens, () => withTimeout(
454
473
  client.generateText(prompt, maxTokens, temperature),
455
- `${client.type}:${client.modelId}`
456
- );
474
+ `${client.type}:${client.modelId}`,
475
+ onHeartbeat
476
+ ));
457
477
  }
458
- function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema) {
459
- return withTimeout(
478
+ function boundedGenerateStructured(client, prompt, maxTokens, temperature, elementSchema, onHeartbeat) {
479
+ return spanned(client, "structured", maxTokens, () => withTimeout(
460
480
  client.generateStructured(prompt, maxTokens, temperature, elementSchema),
461
- `${client.type}:${client.modelId}`
462
- );
481
+ `${client.type}:${client.modelId}`,
482
+ onHeartbeat
483
+ ));
463
484
  }
464
485
 
465
486
  // src/workers/detection/detection-chunking.ts
@@ -990,7 +1011,7 @@ function assertNotTruncated(response, motivation, chunk, totalChunks, outputBudg
990
1011
  throw new Error(`${motivation} detection response truncated (max_tokens) on chunk ${chunk}/${totalChunks} despite the derived output budget of ${outputBudget} tokens \u2014 failing the job rather than under-reporting annotations.`);
991
1012
  }
992
1013
  }
993
- async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onChunk) {
1014
+ async function detectInChunks(client, content, buildPrompt, temperature, motivation, elementSchema, parse, onActivity) {
994
1015
  const limits = await client.limits();
995
1016
  const scaffoldTokens = estimateTokens(buildPrompt(""));
996
1017
  const { chunking, outputBudget } = deriveDetectionBudget(limits, scaffoldTokens);
@@ -1002,12 +1023,14 @@ async function detectInChunks(client, content, buildPrompt, temperature, motivat
1002
1023
  buildPrompt(chunks[i]),
1003
1024
  outputBudget,
1004
1025
  temperature,
1005
- elementSchema
1026
+ elementSchema,
1027
+ // Still alive, same position (a long single call is otherwise silent).
1028
+ () => onActivity?.(i, chunks.length)
1006
1029
  );
1007
1030
  assertNotTruncated(response, motivation, i + 1, chunks.length, outputBudget);
1008
1031
  collected.push(...parse(response.items));
1009
1032
  if (i < chunks.length - 1) {
1010
- onChunk?.(i + 1, chunks.length);
1033
+ onActivity?.(i + 1, chunks.length);
1011
1034
  }
1012
1035
  }
1013
1036
  return collected;
@@ -1021,7 +1044,7 @@ var AnnotationDetection = class {
1021
1044
  * (source-resource locale). See `types.ts` "Locale conventions" for the
1022
1045
  * full discussion.
1023
1046
  */
1024
- static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onChunk) {
1047
+ static async detectComments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
1025
1048
  return detectInChunks(
1026
1049
  client,
1027
1050
  content,
@@ -1030,7 +1053,7 @@ var AnnotationDetection = class {
1030
1053
  "comment",
1031
1054
  COMMENT_ELEMENT_SCHEMA,
1032
1055
  (items) => MotivationParsers.parseComments(items, content),
1033
- onChunk
1056
+ onActivity
1034
1057
  );
1035
1058
  }
1036
1059
  /**
@@ -1040,7 +1063,7 @@ var AnnotationDetection = class {
1040
1063
  * applies, used in the prompt so the LLM analyzes non-English source
1041
1064
  * correctly.
1042
1065
  */
1043
- static async detectHighlights(content, client, instructions, density, sourceLanguage, onChunk) {
1066
+ static async detectHighlights(content, client, instructions, density, sourceLanguage, onActivity) {
1044
1067
  return detectInChunks(
1045
1068
  client,
1046
1069
  content,
@@ -1049,7 +1072,7 @@ var AnnotationDetection = class {
1049
1072
  "highlight",
1050
1073
  HIGHLIGHT_ELEMENT_SCHEMA,
1051
1074
  (items) => MotivationParsers.parseHighlights(items, content),
1052
- onChunk
1075
+ onActivity
1053
1076
  );
1054
1077
  }
1055
1078
  /**
@@ -1059,7 +1082,7 @@ var AnnotationDetection = class {
1059
1082
  * (annotation body locale). `sourceLanguage` is the locale of the content
1060
1083
  * being analyzed (source-resource locale).
1061
1084
  */
1062
- static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onChunk) {
1085
+ static async detectAssessments(content, client, instructions, tone, density, language, sourceLanguage, onActivity) {
1063
1086
  return detectInChunks(
1064
1087
  client,
1065
1088
  content,
@@ -1068,7 +1091,7 @@ var AnnotationDetection = class {
1068
1091
  "assessment",
1069
1092
  ASSESSMENT_ELEMENT_SCHEMA,
1070
1093
  (items) => MotivationParsers.parseAssessments(items, content),
1071
- onChunk
1094
+ onActivity
1072
1095
  );
1073
1096
  }
1074
1097
  /**
@@ -1083,7 +1106,7 @@ var AnnotationDetection = class {
1083
1106
  * identifiers, not LLM-generated text — so it's consumed at the body-stamp
1084
1107
  * site, not here.
1085
1108
  */
1086
- static async detectTags(content, client, schema, category, sourceLanguage, onChunk) {
1109
+ static async detectTags(content, client, schema, category, sourceLanguage, onActivity) {
1087
1110
  const categoryInfo = schema.tags.find((t) => t.name === category);
1088
1111
  if (!categoryInfo) {
1089
1112
  throw new Error(`Invalid category "${category}" for schema ${schema.id}`);
@@ -1105,7 +1128,7 @@ var AnnotationDetection = class {
1105
1128
  "tag",
1106
1129
  TAG_ELEMENT_SCHEMA,
1107
1130
  (items) => MotivationParsers.parseTags(items),
1108
- onChunk
1131
+ onActivity
1109
1132
  );
1110
1133
  return MotivationParsers.validateTagOffsets(parsedTags, content, category);
1111
1134
  }
@@ -1121,7 +1144,7 @@ var ENTITY_ELEMENT_SCHEMA = {
1121
1144
  required: ["exact", "entityType"],
1122
1145
  additionalProperties: false
1123
1146
  };
1124
- async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onChunk) {
1147
+ async function extractEntities(exact, entityTypes, client, includeDescriptiveReferences, logger, sourceLanguage, onActivity) {
1125
1148
  const entityTypesDescription = entityTypes.map((et) => {
1126
1149
  if (typeof et === "string") {
1127
1150
  return et;
@@ -1188,7 +1211,10 @@ Example output:
1188
1211
  outputBudget,
1189
1212
  0.3,
1190
1213
  // Lower temperature for more consistent extraction
1191
- ENTITY_ELEMENT_SCHEMA
1214
+ ENTITY_ELEMENT_SCHEMA,
1215
+ // Still alive, same position: a long single call would otherwise emit
1216
+ // nothing at all between start and finish.
1217
+ () => onActivity?.(i, chunks.length)
1192
1218
  );
1193
1219
  logger.debug("Got entity extraction response", {
1194
1220
  chunk: i + 1,
@@ -1213,7 +1239,7 @@ Example output:
1213
1239
  }
1214
1240
  }
1215
1241
  if (i < chunks.length - 1) {
1216
- onChunk?.(i + 1, chunks.length);
1242
+ onActivity?.(i + 1, chunks.length);
1217
1243
  }
1218
1244
  }
1219
1245
  return collected;
@@ -1557,30 +1583,39 @@ function dedupeAnnotations(annotations) {
1557
1583
  return out;
1558
1584
  }
1559
1585
  async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
1560
- onProgress(10, "Loading resource...", "analyzing");
1561
- onProgress(30, "Analyzing text...", "analyzing");
1586
+ const echo = detectionEcho(params);
1587
+ onProgress(10, { code: "loading" }, echo);
1588
+ onProgress(30, { code: "analyzing" }, echo);
1562
1589
  const highlights = await AnnotationDetection.detectHighlights(
1563
1590
  content,
1564
1591
  inferenceClient,
1565
1592
  params.instructions,
1566
1593
  params.density,
1567
1594
  params.sourceLanguage,
1568
- // Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
1569
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
1595
+ // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
1596
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
1570
1597
  );
1571
- onProgress(60, `Creating ${highlights.length} annotations...`, "creating");
1598
+ onProgress(60, { code: "creating-annotations", count: highlights.length }, echo);
1572
1599
  const annotations = dedupeAnnotations(highlights.map(
1573
1600
  (h) => buildAnnotation("highlighting", h)
1574
1601
  ));
1575
- onProgress(100, `Complete! Created ${annotations.length} highlights`, "creating");
1602
+ onProgress(100, { code: "complete-created", count: annotations.length, kind: "highlight" }, echo);
1576
1603
  return {
1577
1604
  annotations,
1578
1605
  result: { highlightsFound: highlights.length, highlightsCreated: annotations.length }
1579
1606
  };
1580
1607
  }
1608
+ function detectionEcho(p) {
1609
+ const requestParams = [];
1610
+ if (p.instructions?.trim()) requestParams.push({ label: "instructions", value: p.instructions.trim() });
1611
+ if (p.tone?.trim()) requestParams.push({ label: "tone", value: p.tone.trim() });
1612
+ if (p.density !== void 0) requestParams.push({ label: "density", value: String(p.density) });
1613
+ return requestParams.length > 0 ? { requestParams } : {};
1614
+ }
1581
1615
  async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
1582
- onProgress(10, "Loading resource...", "analyzing");
1583
- onProgress(30, "Analyzing text...", "analyzing");
1616
+ const echo = detectionEcho(params);
1617
+ onProgress(10, { code: "loading" }, echo);
1618
+ onProgress(30, { code: "analyzing" }, echo);
1584
1619
  const comments = await AnnotationDetection.detectComments(
1585
1620
  content,
1586
1621
  inferenceClient,
@@ -1589,10 +1624,10 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
1589
1624
  params.density,
1590
1625
  params.language,
1591
1626
  params.sourceLanguage,
1592
- // Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
1593
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
1627
+ // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
1628
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
1594
1629
  );
1595
- onProgress(60, `Creating ${comments.length} annotations...`, "creating");
1630
+ onProgress(60, { code: "creating-annotations", count: comments.length }, echo);
1596
1631
  const bodyLanguage = params.language ?? "en";
1597
1632
  const annotations = dedupeAnnotations(comments.map(
1598
1633
  (c) => (
@@ -1604,15 +1639,16 @@ async function processCommentJob(content, inferenceClient, params, buildAnnotati
1604
1639
  ])
1605
1640
  )
1606
1641
  ));
1607
- onProgress(100, `Complete! Created ${annotations.length} comments`, "creating");
1642
+ onProgress(100, { code: "complete-created", count: annotations.length, kind: "comment" }, echo);
1608
1643
  return {
1609
1644
  annotations,
1610
1645
  result: { commentsFound: comments.length, commentsCreated: annotations.length }
1611
1646
  };
1612
1647
  }
1613
1648
  async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
1614
- onProgress(10, "Loading resource...", "analyzing");
1615
- onProgress(30, "Analyzing text...", "analyzing");
1649
+ const echo = detectionEcho(params);
1650
+ onProgress(10, { code: "loading" }, echo);
1651
+ onProgress(30, { code: "analyzing" }, echo);
1616
1652
  const assessments = await AnnotationDetection.detectAssessments(
1617
1653
  content,
1618
1654
  inferenceClient,
@@ -1621,10 +1657,10 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
1621
1657
  params.density,
1622
1658
  params.language,
1623
1659
  params.sourceLanguage,
1624
- // Chunk-boundary heartbeat (liveness): interpolate within the 30–60 band.
1625
- (completed, total) => onProgress(30 + Math.round(completed / total * 30), "Analyzing text...", "analyzing")
1660
+ // Liveness (chunk boundaries + in-flight heartbeat): 30–60 band.
1661
+ (completed, total) => onProgress(30 + Math.round(completed / total * 30), { code: "analyzing" }, echo)
1626
1662
  );
1627
- onProgress(60, `Creating ${assessments.length} annotations...`, "creating");
1663
+ onProgress(60, { code: "creating-annotations", count: assessments.length }, echo);
1628
1664
  const bodyLanguage = params.language ?? "en";
1629
1665
  const annotations = dedupeAnnotations(assessments.map(
1630
1666
  (a) => (
@@ -1643,7 +1679,7 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
1643
1679
  })
1644
1680
  )
1645
1681
  ));
1646
- onProgress(100, `Complete! Created ${annotations.length} assessments`, "creating");
1682
+ onProgress(100, { code: "complete-created", count: annotations.length, kind: "assessment" }, echo);
1647
1683
  return {
1648
1684
  annotations,
1649
1685
  result: { assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
@@ -1651,25 +1687,27 @@ async function processAssessmentJob(content, inferenceClient, params, buildAnnot
1651
1687
  }
1652
1688
  async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger) {
1653
1689
  const entityTypeNames = params.entityTypes.map(String);
1654
- const requestParams = [{ label: "Entity types", value: entityTypeNames.join(", ") }];
1655
- const completedEntityTypes = [];
1690
+ const requestParams = [{ label: "entity-types", value: entityTypeNames.join(", ") }];
1691
+ const completedItems = [];
1656
1692
  let totalFound = 0;
1657
1693
  let totalEmitted = 0;
1658
1694
  let errors = 0;
1659
1695
  const allAnnotations = [];
1660
- onProgress(10, "Loading resource...", "analyzing", { requestParams });
1696
+ onProgress(10, { code: "loading" }, { requestParams });
1661
1697
  const bodyLanguage = params.language ?? "en";
1662
1698
  for (let i = 0; i < entityTypeNames.length; i++) {
1663
1699
  const entityTypeName = entityTypeNames[i];
1664
1700
  if (!entityTypeName) continue;
1665
1701
  const pct = 20 + Math.round(i / entityTypeNames.length * 60);
1666
- onProgress(pct, `Detecting ${entityTypeName} entities...`, "analyzing", {
1667
- currentEntityType: entityTypeName,
1668
- processedEntityTypes: i,
1669
- totalEntityTypes: entityTypeNames.length,
1702
+ onProgress(pct, { code: "detecting-entities", entityType: entityTypeName }, {
1703
+ // One vocabulary for "what is in flight" (CLEAN-PROGRESS D2): the entity
1704
+ // type is KB data, `kind` is the code the client localizes around it.
1705
+ current: { kind: "entity-type", value: entityTypeName },
1706
+ processed: i,
1707
+ total: entityTypeNames.length,
1670
1708
  entitiesFound: totalFound,
1671
1709
  entitiesEmitted: totalEmitted,
1672
- completedEntityTypes: [...completedEntityTypes],
1710
+ completedItems: [...completedItems],
1673
1711
  requestParams
1674
1712
  });
1675
1713
  const extractedEntities = await extractEntities(
@@ -1679,25 +1717,28 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
1679
1717
  params.includeDescriptiveReferences ?? false,
1680
1718
  logger,
1681
1719
  params.sourceLanguage,
1682
- // Chunk-boundary heartbeat: progress is the worker's liveness signal
1683
- // (stall watchdog + backend janitor), so multi-chunk extraction must
1684
- // emit between inference calls. Percentage interpolates within this
1685
- // entity type's band of the 20–80 range.
1720
+ // Liveness: fires at chunk boundaries AND every ~15 s while a single
1721
+ // inference call is in flight (DETECTION-HEARTBEAT). Progress feeds the
1722
+ // stall watchdog, the janitor, AND the client's inter-emission timeout,
1723
+ // so a long single-chunk call must not be silent. Percentage
1724
+ // interpolates within this entity type's band of the 20–80 range; a
1725
+ // heartbeat repeats the current position rather than inventing an
1726
+ // advance.
1686
1727
  (completed, total) => {
1687
1728
  const interpolated = 20 + Math.round((i + completed / total) / entityTypeNames.length * 60);
1688
- onProgress(interpolated, `Detecting ${entityTypeName} entities...`, "analyzing", {
1689
- currentEntityType: entityTypeName,
1690
- processedEntityTypes: i,
1691
- totalEntityTypes: entityTypeNames.length,
1729
+ onProgress(interpolated, { code: "detecting-entities", entityType: entityTypeName }, {
1730
+ current: { kind: "entity-type", value: entityTypeName },
1731
+ processed: i,
1732
+ total: entityTypeNames.length,
1692
1733
  entitiesFound: totalFound,
1693
1734
  entitiesEmitted: totalEmitted,
1694
- completedEntityTypes: [...completedEntityTypes],
1735
+ completedItems: [...completedItems],
1695
1736
  requestParams
1696
1737
  });
1697
1738
  }
1698
1739
  );
1699
1740
  totalFound += extractedEntities.length;
1700
- completedEntityTypes.push({ entityType: entityTypeName, foundCount: extractedEntities.length });
1741
+ completedItems.push({ value: entityTypeName, foundCount: extractedEntities.length });
1701
1742
  const unresolvedBody = [
1702
1743
  { type: "TextualBody", value: entityTypeName, purpose: "tagging", format: "text/plain", language: bodyLanguage }
1703
1744
  ];
@@ -1728,36 +1769,49 @@ async function processReferenceJob(content, inferenceClient, params, buildAnnota
1728
1769
  }
1729
1770
  }
1730
1771
  const annotations = dedupeAnnotations(allAnnotations);
1731
- onProgress(100, `Complete! Created ${annotations.length} references`, "creating");
1772
+ onProgress(100, { code: "complete-created", count: annotations.length, kind: "reference" }, { requestParams });
1732
1773
  return {
1733
1774
  annotations,
1734
1775
  result: { totalFound, totalEmitted: annotations.length, errors }
1735
1776
  };
1736
1777
  }
1737
1778
  async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
1738
- onProgress(10, "Loading resource...", "analyzing");
1739
- onProgress(30, "Analyzing text for tags...", "analyzing");
1779
+ onProgress(10, { code: "loading" });
1780
+ onProgress(30, { code: "analyzing-tags" });
1740
1781
  const allTags = [];
1782
+ const completedItems = [];
1741
1783
  for (let c = 0; c < params.categories.length; c++) {
1742
1784
  const category = params.categories[c];
1785
+ const position = () => ({
1786
+ current: { kind: "category", value: category },
1787
+ processed: c,
1788
+ total: params.categories.length,
1789
+ completedItems: [...completedItems]
1790
+ });
1791
+ onProgress(
1792
+ 30 + Math.round(c / params.categories.length * 30),
1793
+ { code: "analyzing-tags" },
1794
+ position()
1795
+ );
1743
1796
  const categoryTags = await AnnotationDetection.detectTags(
1744
1797
  content,
1745
1798
  inferenceClient,
1746
1799
  params.schema,
1747
1800
  category,
1748
1801
  params.sourceLanguage,
1749
- // Chunk-boundary heartbeat (liveness): interpolate within this
1750
- // category's slice of the 30–60 band.
1802
+ // Liveness (chunk boundaries + in-flight heartbeat): this category's
1803
+ // slice of the 30–60 band.
1751
1804
  (completed, total) => onProgress(
1752
1805
  30 + Math.round((c + completed / total) / params.categories.length * 30),
1753
- "Analyzing text for tags...",
1754
- "analyzing"
1806
+ { code: "analyzing-tags" },
1807
+ position()
1755
1808
  )
1756
1809
  );
1810
+ completedItems.push({ value: category, foundCount: categoryTags.length });
1757
1811
  allTags.push(...categoryTags);
1758
1812
  }
1759
1813
  const tags = allTags;
1760
- onProgress(60, `Creating ${tags.length} tag annotations...`, "creating");
1814
+ onProgress(60, { code: "creating-tag-annotations", count: tags.length });
1761
1815
  const bodyLanguage = params.language ?? "en";
1762
1816
  const annotations = dedupeAnnotations(tags.map((t) => {
1763
1817
  const category = t.category ?? "unknown";
@@ -1772,7 +1826,7 @@ async function processTagJob(content, inferenceClient, params, buildAnnotation,
1772
1826
  const category = Array.isArray(body) && typeof body[0]?.value === "string" ? body[0].value : "unknown";
1773
1827
  byCategory[category] = (byCategory[category] ?? 0) + 1;
1774
1828
  }
1775
- onProgress(100, `Complete! Created ${annotations.length} tags`, "creating");
1829
+ onProgress(100, { code: "complete-created", count: annotations.length, kind: "tag" });
1776
1830
  return {
1777
1831
  annotations,
1778
1832
  result: { tagsFound: tags.length, tagsCreated: annotations.length, byCategory }
@@ -1795,7 +1849,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
1795
1849
  const title = params.title ?? "Untitled";
1796
1850
  const entityTypes = (params.entityTypes ?? []).map(String);
1797
1851
  if (outputMediaType === "application/pdf") {
1798
- onProgress(5, "Generating resource...", "generating");
1852
+ onProgress(5, { code: "generating-resource" });
1799
1853
  const validIds = params.cite === true ? collectContextResourceIds(params.context) : null;
1800
1854
  let generated2 = await generateResourceFromTopic(
1801
1855
  title,
@@ -1860,7 +1914,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
1860
1914
  );
1861
1915
  }
1862
1916
  assertWithinOutputBudget(compiled.pdf.byteLength);
1863
- onProgress(95, "Creating resource...", "creating");
1917
+ onProgress(95, { code: "creating-resource" });
1864
1918
  return {
1865
1919
  content: compiled.pdf,
1866
1920
  title: generated2.title ?? title,
@@ -1872,7 +1926,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
1872
1926
  }
1873
1927
  };
1874
1928
  }
1875
- onProgress(5, "Generating resource...", "generating");
1929
+ onProgress(5, { code: "generating-resource" });
1876
1930
  const generated = await generateResourceFromTopic(
1877
1931
  title,
1878
1932
  entityTypes,
@@ -1896,7 +1950,7 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger)
1896
1950
  content = resolved.content;
1897
1951
  citations = resolved.citations;
1898
1952
  }
1899
- onProgress(95, "Creating resource...", "creating");
1953
+ onProgress(95, { code: "creating-resource" });
1900
1954
  const artifact = new TextEncoder().encode(content);
1901
1955
  assertWithinOutputBudget(artifact.byteLength);
1902
1956
  return {