@sembl/core 0.3.0 → 0.4.0

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
@@ -1,3 +1,155 @@
1
+ // src/schema/formats.ts
2
+ var FIELD_FORMATS = [
3
+ "url",
4
+ "email",
5
+ "date",
6
+ "datetime",
7
+ "iso-country",
8
+ "us-state",
9
+ "us-state-name",
10
+ "currency"
11
+ ];
12
+ var ISO_COUNTRIES = new Set(
13
+ "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW".split(" ")
14
+ );
15
+ var US_STATES = {
16
+ AL: "Alabama",
17
+ AK: "Alaska",
18
+ AZ: "Arizona",
19
+ AR: "Arkansas",
20
+ CA: "California",
21
+ CO: "Colorado",
22
+ CT: "Connecticut",
23
+ DE: "Delaware",
24
+ FL: "Florida",
25
+ GA: "Georgia",
26
+ HI: "Hawaii",
27
+ ID: "Idaho",
28
+ IL: "Illinois",
29
+ IN: "Indiana",
30
+ IA: "Iowa",
31
+ KS: "Kansas",
32
+ KY: "Kentucky",
33
+ LA: "Louisiana",
34
+ ME: "Maine",
35
+ MD: "Maryland",
36
+ MA: "Massachusetts",
37
+ MI: "Michigan",
38
+ MN: "Minnesota",
39
+ MS: "Mississippi",
40
+ MO: "Missouri",
41
+ MT: "Montana",
42
+ NE: "Nebraska",
43
+ NV: "Nevada",
44
+ NH: "New Hampshire",
45
+ NJ: "New Jersey",
46
+ NM: "New Mexico",
47
+ NY: "New York",
48
+ NC: "North Carolina",
49
+ ND: "North Dakota",
50
+ OH: "Ohio",
51
+ OK: "Oklahoma",
52
+ OR: "Oregon",
53
+ PA: "Pennsylvania",
54
+ RI: "Rhode Island",
55
+ SC: "South Carolina",
56
+ SD: "South Dakota",
57
+ TN: "Tennessee",
58
+ TX: "Texas",
59
+ UT: "Utah",
60
+ VT: "Vermont",
61
+ VA: "Virginia",
62
+ WA: "Washington",
63
+ WV: "West Virginia",
64
+ WI: "Wisconsin",
65
+ WY: "Wyoming",
66
+ DC: "District of Columbia",
67
+ PR: "Puerto Rico",
68
+ GU: "Guam",
69
+ VI: "U.S. Virgin Islands",
70
+ AS: "American Samoa",
71
+ MP: "Northern Mariana Islands"
72
+ };
73
+ var US_STATE_NAMES = new Set(Object.values(US_STATES));
74
+ var CURRENCIES = new Set(
75
+ "AED AFN ALL AMD ANG AOA ARS AUD AWG AZN BAM BBD BDT BGN BHD BIF BMD BND BOB BRL BSD BTN BWP BYN BZD CAD CDF CHF CLP CNY COP CRC CUP CVE CZK DJF DKK DOP DZD EGP ERN ETB EUR FJD FKP GBP GEL GHS GIP GMD GNF GTQ GYD HKD HNL HTG HUF IDR ILS INR IQD IRR ISK JMD JOD JPY KES KGS KHR KMF KPW KRW KWD KYD KZT LAK LBP LKR LRD LSL LYD MAD MDL MGA MKD MMK MNT MOP MRU MUR MVR MWK MXN MYR MZN NAD NGN NIO NOK NPR NZD OMR PAB PEN PGK PHP PKR PLN PYG QAR RON RSD RUB RWF SAR SBD SCR SDG SEK SGD SHP SLE SOS SRD SSP STN SVC SYP SZL THB TJS TMT TND TOP TRY TTD TWD TZS UAH UGX USD UYU UZS VES VND VUV WST XAF XCD XOF XPF YER ZAR ZMW ZWG".split(" ")
76
+ );
77
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
78
+ var DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
79
+ var DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
80
+ function isCalendarDate(y, m, d) {
81
+ const date = new Date(Date.UTC(y, m - 1, d));
82
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
83
+ }
84
+ function validateFormat(value, format) {
85
+ switch (format) {
86
+ case "url": {
87
+ try {
88
+ const url = new URL(value);
89
+ if (url.protocol === "http:" || url.protocol === "https:") return void 0;
90
+ } catch {
91
+ }
92
+ return `Expected an absolute http(s) URL, got ${JSON.stringify(value)}`;
93
+ }
94
+ case "email":
95
+ return EMAIL.test(value) ? void 0 : `Expected an email address, got ${JSON.stringify(value)}`;
96
+ case "date": {
97
+ const m = DATE.exec(value);
98
+ if (m && isCalendarDate(Number(m[1]), Number(m[2]), Number(m[3]))) return void 0;
99
+ return `Expected a calendar date as YYYY-MM-DD, got ${JSON.stringify(value)}`;
100
+ }
101
+ case "datetime":
102
+ return DATETIME.test(value) && !Number.isNaN(Date.parse(value)) ? void 0 : `Expected an ISO 8601 timestamp, got ${JSON.stringify(value)}`;
103
+ case "iso-country":
104
+ return ISO_COUNTRIES.has(value) ? void 0 : `Expected an ISO 3166-1 alpha-2 country code such as US or DE, got ${JSON.stringify(value)}`;
105
+ case "us-state":
106
+ return value in US_STATES ? void 0 : `Expected a two-letter USPS state code such as CA or NY, got ${JSON.stringify(value)}`;
107
+ case "us-state-name":
108
+ return US_STATE_NAMES.has(value) ? void 0 : `Expected a US state's full name such as California, got ${JSON.stringify(value)}`;
109
+ case "currency":
110
+ return CURRENCIES.has(value) ? void 0 : `Expected an ISO 4217 currency code such as USD or EUR, got ${JSON.stringify(value)}`;
111
+ }
112
+ }
113
+ function describeFormat(format) {
114
+ switch (format) {
115
+ case "url":
116
+ return "an absolute http(s) URL";
117
+ case "email":
118
+ return "an email address";
119
+ case "date":
120
+ return "a calendar date as YYYY-MM-DD";
121
+ case "datetime":
122
+ return "an ISO 8601 timestamp (e.g. 2026-09-05T14:30:00Z)";
123
+ case "iso-country":
124
+ return "an ISO 3166-1 alpha-2 country code (e.g. US, DE, PT), never a country name";
125
+ case "us-state":
126
+ return "a two-letter USPS state code (e.g. CA, NY), never the state's name";
127
+ case "us-state-name":
128
+ return "a US state's full name (e.g. California, New York), never its abbreviation";
129
+ case "currency":
130
+ return "an ISO 4217 currency code (e.g. USD, EUR, GBP), never a symbol or a word";
131
+ }
132
+ }
133
+ function formatToJsonSchema(format) {
134
+ switch (format) {
135
+ case "url":
136
+ return { format: "uri" };
137
+ case "email":
138
+ return { format: "email" };
139
+ case "date":
140
+ return { format: "date" };
141
+ case "datetime":
142
+ return { format: "date-time" };
143
+ case "iso-country":
144
+ case "us-state":
145
+ return { pattern: "^[A-Z]{2}$" };
146
+ case "currency":
147
+ return { pattern: "^[A-Z]{3}$" };
148
+ case "us-state-name":
149
+ return {};
150
+ }
151
+ }
152
+
1
153
  // src/schema/json-schema.ts
2
154
  var CONSTRAINT_KEYWORDS = [
3
155
  "maxLength",
@@ -14,6 +166,9 @@ function constraintsToJsonSchema(constraints, dialect) {
14
166
  return {};
15
167
  }
16
168
  const out = {};
169
+ if (constraints.format !== void 0) {
170
+ Object.assign(out, formatToJsonSchema(constraints.format));
171
+ }
17
172
  for (const keyword of CONSTRAINT_KEYWORDS) {
18
173
  const value = constraints[keyword];
19
174
  if (value !== void 0) {
@@ -398,7 +553,7 @@ ${summary}`);
398
553
  // src/coerce/sources.ts
399
554
  var SOURCE_TAG = "source";
400
555
  function isSource(value) {
401
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.text === "string" && (value.label === void 0 || typeof value.label === "string");
556
+ return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.text === "string" && (value.label === void 0 || typeof value.label === "string") && (value.maxChars === void 0 || typeof value.maxChars === "number");
402
557
  }
403
558
  function isCoerceInput(value) {
404
559
  return typeof value === "string" || isSource(value) || Array.isArray(value) && value.every(isSource);
@@ -418,7 +573,9 @@ function toSources(input) {
418
573
  }
419
574
  function cleanLabel(source) {
420
575
  const label = source.label?.trim();
421
- return label ? { label, text: source.text } : { text: source.text };
576
+ const cleaned = label ? { label, text: source.text } : { text: source.text };
577
+ if (source.maxChars !== void 0) cleaned.maxChars = source.maxChars;
578
+ return cleaned;
422
579
  }
423
580
  function escapeText(text) {
424
581
  return text.replace(new RegExp(`</(\\s*${SOURCE_TAG}\\b)`, "gi"), "<\\/$1");
@@ -456,7 +613,10 @@ function describeConstraints(constraints) {
456
613
  return [];
457
614
  }
458
615
  const phrases = [];
459
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
616
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
617
+ if (format !== void 0) {
618
+ phrases.push(describeFormat(format));
619
+ }
460
620
  if (minLength !== void 0 && maxLength !== void 0) {
461
621
  phrases.push(`between ${minLength} and ${maxLength} characters`);
462
622
  } else if (maxLength !== void 0) {
@@ -569,6 +729,8 @@ function buildPrompt(schema, bundle, options = {}) {
569
729
  lines.push("Instructions:");
570
730
  lines.push("- Extract values from the sources that match the schema fields.");
571
731
  lines.push("- Use null for optional fields that cannot be determined from the input.");
732
+ lines.push("- A value the input states is never omitted because it looks like a default: return 1, 0, false or an empty list when that is what the input says.");
733
+ lines.push("- Never return an empty object when the input states values for any field.");
572
734
  lines.push("- Required fields must always have a valid, non-null value.");
573
735
  lines.push("- Interpret the user's input semantically \u2014 infer meaning, don't just pattern match.");
574
736
  lines.push("- Respect every stated limit exactly; truncate or drop lower-priority content to stay within it.");
@@ -634,11 +796,31 @@ var PROVENANCE_INSTRUCTIONS = [
634
796
  "- Judge each field on its own. A confident value next to a guessed one is",
635
797
  " normal, and marking the guess honestly is more useful than looking sure."
636
798
  ].join("\n");
799
+ function provenanceFieldNames(schema, fields) {
800
+ if (fields === void 0) return new Set(schema.fields.map((f) => f.name));
801
+ const known = new Set(schema.fields.map((f) => f.name));
802
+ for (const name of fields) {
803
+ if (!known.has(name)) {
804
+ throw new RangeError(
805
+ `provenance field "${name}" is not a field of schema "${schema.id}" (fields: ${[...known].join(", ")})`
806
+ );
807
+ }
808
+ }
809
+ return new Set(fields);
810
+ }
637
811
  function provenanceInstructions(options = {}) {
638
812
  const labels = options.sourceLabels ?? [];
639
- if (labels.length < 2) return PROVENANCE_INSTRUCTIONS;
640
- return `${PROVENANCE_INSTRUCTIONS}
641
- - Set \`source\` to the label of the source the value was read from.`;
813
+ let text = PROVENANCE_INSTRUCTIONS;
814
+ if (options.fields !== void 0) {
815
+ text = text.replace(
816
+ "- Every field is wrapped as an object: put the extracted value in `value`.",
817
+ `- Only these fields are wrapped as objects, with the extracted value in \`value\`: ${options.fields.join(", ")}. Every other field is a plain value.`
818
+ );
819
+ }
820
+ if (labels.length >= 2) {
821
+ text += "\n- Always set `source` to the label of the source the value was read from. When several agree, name the one quoted in `evidence`.";
822
+ }
823
+ return text;
642
824
  }
643
825
  function annotationSchema(parentId, field2, sourceLabels) {
644
826
  const valueField = {
@@ -670,7 +852,7 @@ function annotationSchema(parentId, field2, sourceLabels) {
670
852
  name: "source",
671
853
  description: "The label of the source this value was read from.",
672
854
  type: { kind: "enum", values: [...sourceLabels] },
673
- required: false
855
+ required: true
674
856
  }
675
857
  ] : []
676
858
  ]
@@ -680,7 +862,12 @@ function toProvenanceSchema(schema, bundle, options = {}) {
680
862
  const schemas = { ...bundle?.schemas ?? {} };
681
863
  const fields = [];
682
864
  const sourceLabels = options.sourceLabels ?? [];
865
+ const wrapped = provenanceFieldNames(schema, options.fields);
683
866
  for (const field2 of schema.fields) {
867
+ if (!wrapped.has(field2.name)) {
868
+ fields.push(field2);
869
+ continue;
870
+ }
684
871
  const annotation = annotationSchema(schema.id, field2, sourceLabels);
685
872
  schemas[annotation.id] = annotation;
686
873
  fields.push({
@@ -756,10 +943,30 @@ ${tailRoom > 0 ? text.slice(text.length - tailRoom) : ""}`;
756
943
  }
757
944
  }
758
945
  function budgetSources(sources, maxChars, policy = "tail") {
759
- const total = sources.reduce((sum, s) => sum + s.text.length, 0);
760
- if (total <= maxChars) {
761
- return { sources: [...sources], truncated: [] };
946
+ const records = /* @__PURE__ */ new Map();
947
+ const record = (index, source, text) => {
948
+ const existing = records.get(index);
949
+ if (existing) {
950
+ existing.keptLength = text.length;
951
+ } else {
952
+ records.set(index, {
953
+ ...source.label !== void 0 ? { label: source.label } : {},
954
+ originalLength: source.text.length,
955
+ keptLength: text.length
956
+ });
957
+ }
958
+ };
959
+ const capped = sources.map((source, index) => {
960
+ if (source.maxChars === void 0 || source.text.length <= source.maxChars) return source;
961
+ const text = truncateText(source.text, source.maxChars, policy);
962
+ record(index, source, text);
963
+ return { ...source, text };
964
+ });
965
+ const total = capped.reduce((sum, s) => sum + s.text.length, 0);
966
+ if (maxChars === void 0 || total <= maxChars) {
967
+ return { sources: capped, truncated: [...records.values()] };
762
968
  }
969
+ sources = capped;
763
970
  const allowance = /* @__PURE__ */ new Map();
764
971
  const order = sources.map((s, i) => i).sort((a, b) => sources[a].text.length - sources[b].text.length);
765
972
  let remaining = maxChars;
@@ -769,19 +976,14 @@ function budgetSources(sources, maxChars, policy = "tail") {
769
976
  allowance.set(index, granted);
770
977
  remaining -= granted;
771
978
  });
772
- const truncated = [];
773
979
  const budgeted = sources.map((source, index) => {
774
980
  const limit = allowance.get(index) ?? 0;
775
981
  if (source.text.length <= limit) return source;
776
982
  const text = truncateText(source.text, limit, policy);
777
- truncated.push({
778
- ...source.label !== void 0 ? { label: source.label } : {},
779
- originalLength: source.text.length,
780
- keptLength: text.length
781
- });
983
+ record(index, source, text);
782
984
  return { ...source, text };
783
985
  });
784
- return { sources: budgeted, truncated };
986
+ return { sources: budgeted, truncated: [...records.values()] };
785
987
  }
786
988
 
787
989
  // src/coerce/validator.ts
@@ -797,7 +999,7 @@ function entries(count) {
797
999
  return `${count} ${count === 1 ? "entry" : "entries"}`;
798
1000
  }
799
1001
  function validateConstraints(value, constraints, path, issues) {
800
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
1002
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
801
1003
  if (Array.isArray(value)) {
802
1004
  if (minItems !== void 0 && value.length < minItems) {
803
1005
  issues.push({
@@ -841,6 +1043,10 @@ function validateConstraints(value, constraints, path, issues) {
841
1043
  received: value
842
1044
  });
843
1045
  }
1046
+ if (format !== void 0) {
1047
+ const message = validateFormat(value, format);
1048
+ if (message) issues.push({ path, message, received: value });
1049
+ }
844
1050
  return;
845
1051
  }
846
1052
  if (typeof value === "number") {
@@ -1182,8 +1388,15 @@ function generateSpanId() {
1182
1388
  }
1183
1389
  var Tracer = class {
1184
1390
  sinks;
1185
- constructor(sinks) {
1391
+ baseAttributes;
1392
+ /**
1393
+ * `baseAttributes` are merged into every span this tracer opens — how a
1394
+ * batch stamps `itemIndex` on the spans of each item, so a sink can tell
1395
+ * whose `llmCall` it is looking at under concurrency.
1396
+ */
1397
+ constructor(sinks, baseAttributes) {
1186
1398
  this.sinks = sinks ?? [];
1399
+ this.baseAttributes = baseAttributes;
1187
1400
  }
1188
1401
  startSpan(name, attributes, parent) {
1189
1402
  return {
@@ -1191,7 +1404,7 @@ var Tracer = class {
1191
1404
  name,
1192
1405
  startTime: Date.now(),
1193
1406
  events: [],
1194
- attributes,
1407
+ attributes: this.baseAttributes ? { ...this.baseAttributes, ...attributes } : attributes,
1195
1408
  parentId: parent?.id
1196
1409
  };
1197
1410
  }
@@ -1246,15 +1459,31 @@ async function resolveEnums(schema, bundle, enumResolver, tracer, parent) {
1246
1459
  tracer.endSpan(span);
1247
1460
  }
1248
1461
  }
1249
- async function runCoercion(input, options, { mode, provenance }) {
1250
- const { provider, schema, enumResolver, traceSinks } = options;
1251
- const bundle = options.bundle ?? bundleOf(schema);
1462
+ function emptyUsage() {
1463
+ return { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
1464
+ }
1465
+ function addUsage(into, usage) {
1466
+ into.calls += 1;
1467
+ if (!usage) return;
1468
+ into.promptTokens += usage.promptTokens;
1469
+ into.completionTokens += usage.completionTokens;
1470
+ into.totalTokens += usage.totalTokens;
1471
+ into.cacheReadTokens += usage.cacheReadTokens ?? 0;
1472
+ into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
1473
+ }
1474
+ function checkOptions(options) {
1252
1475
  const maxRepairAttempts = options.maxRepairAttempts ?? 0;
1253
1476
  if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
1254
1477
  throw new RangeError(
1255
1478
  `maxRepairAttempts must be a non-negative integer, got ${String(options.maxRepairAttempts)}`
1256
1479
  );
1257
1480
  }
1481
+ const retryOnEmpty = options.retryOnEmpty ?? 0;
1482
+ if (!Number.isInteger(retryOnEmpty) || retryOnEmpty < 0) {
1483
+ throw new RangeError(
1484
+ `retryOnEmpty must be a non-negative integer, got ${String(options.retryOnEmpty)}`
1485
+ );
1486
+ }
1258
1487
  const instructions = normalizeInstructions(options.instructions);
1259
1488
  const onInvalidField = options.onInvalidField ?? "throw";
1260
1489
  if (!INVALID_FIELD_POLICIES.includes(onInvalidField)) {
@@ -1267,61 +1496,96 @@ async function runCoercion(input, options, { mode, provenance }) {
1267
1496
  `maxInputChars must be a positive integer, got ${String(options.maxInputChars)}`
1268
1497
  );
1269
1498
  }
1499
+ return { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions };
1500
+ }
1501
+ async function prepareRequest(options, { mode, provenance }, instructions, sourceLabels, tracer, rootSpan) {
1502
+ const { schema, enumResolver } = options;
1503
+ const bundle = options.bundle ?? bundleOf(schema);
1504
+ const resolvedEnums = await resolveEnums(schema, bundle, enumResolver, tracer, rootSpan);
1505
+ const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1506
+ const basePrompt = buildPrompt(schema, bundle, { resolvedEnums, instructions });
1507
+ const provenanceOptions = { sourceLabels, fields: options.provenanceFields };
1508
+ const systemPrompt = provenance ? `${basePrompt}
1509
+ ${provenanceInstructions(provenanceOptions)}` : basePrompt;
1510
+ tracer.addEvent(promptSpan, "promptBuilt", {
1511
+ promptLength: systemPrompt.length,
1512
+ instructionCount: instructions.length,
1513
+ mode
1514
+ });
1515
+ tracer.endSpan(promptSpan);
1516
+ const request = provenance ? toProvenanceSchema(schema, bundle, provenanceOptions) : { schema, bundle };
1517
+ const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1518
+ const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1519
+ resolvedEnums
1520
+ });
1521
+ tracer.endSpan(schemaSpan);
1522
+ return { systemPrompt, jsonSchema, schema: request.schema, bundle: request.bundle, resolvedEnums };
1523
+ }
1524
+ function isEmptyResult(data) {
1525
+ return Object.values(data).every((value) => value === null || value === void 0);
1526
+ }
1527
+ var EMPTY_RETRY_NOTE = "A previous attempt at this extraction returned no fields. The sources above do state values for at least some fields; read them again and return every value that is stated, leaving out only what the sources genuinely do not say.";
1528
+ async function runCoercion(input, options, { mode, provenance, traceAttributes }) {
1529
+ const { provider, schema, traceSinks } = options;
1530
+ const bundle = options.bundle ?? bundleOf(schema);
1531
+ const { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions } = checkOptions(options);
1270
1532
  const rawSources = toSources(input);
1271
- const tracer = new Tracer(traceSinks);
1533
+ const tracer = new Tracer(traceSinks, traceAttributes);
1272
1534
  const rootSpan = tracer.startSpan(mode, {
1273
1535
  schemaId: schema.id,
1274
1536
  provenance,
1275
1537
  onInvalidField,
1276
1538
  sourceCount: rawSources.length
1277
1539
  });
1540
+ const usage = emptyUsage();
1278
1541
  try {
1279
1542
  const sources = await prepareSources(rawSources, options, tracer, rootSpan);
1280
1543
  const sourceLabels = sources.length > 1 ? sources.map((s) => s.label ?? "") : [];
1281
- const resolvedEnums = await resolveEnums(
1282
- schema,
1283
- bundle,
1284
- enumResolver,
1544
+ const prepared = await prepareRequest(
1545
+ options,
1546
+ { mode, provenance },
1547
+ instructions,
1548
+ sourceLabels,
1285
1549
  tracer,
1286
1550
  rootSpan
1287
1551
  );
1288
- const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1289
- const basePrompt = buildPrompt(schema, bundle, { resolvedEnums, instructions });
1290
- const systemPrompt = provenance ? `${basePrompt}
1291
- ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1292
- tracer.addEvent(promptSpan, "promptBuilt", {
1293
- promptLength: systemPrompt.length,
1294
- instructionCount: instructions.length
1295
- });
1296
- tracer.endSpan(promptSpan);
1297
- const request = provenance ? toProvenanceSchema(schema, bundle, { sourceLabels }) : { schema, bundle };
1298
- const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1299
- const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1300
- resolvedEnums
1301
- });
1302
- tracer.endSpan(schemaSpan);
1552
+ const { systemPrompt, jsonSchema, resolvedEnums } = prepared;
1303
1553
  const validate = mode === "coerce" ? validateStrict : validatePartial;
1304
1554
  const renderedInput = renderSources(sources);
1555
+ const hasInput = sources.some((s) => s.text.trim().length > 0);
1305
1556
  tracer.addEvent(rootSpan, "inputRendered", {
1306
1557
  sourceCount: sources.length,
1307
1558
  inputLength: renderedInput.length
1308
1559
  });
1309
1560
  let userInput = renderedInput;
1310
1561
  let issues = [];
1311
- let run = { data: {}, provenance: {}, issues: [] };
1562
+ let run = { data: {}, provenance: {}, issues: [], usage };
1563
+ let emptyRetries = 0;
1312
1564
  for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
1313
1565
  const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
1314
1566
  const response = await provider.complete({
1315
1567
  systemPrompt,
1316
1568
  userInput,
1317
1569
  jsonSchema,
1318
- schema: request.schema,
1319
- bundle: request.bundle,
1570
+ schema: prepared.schema,
1571
+ bundle: prepared.bundle,
1320
1572
  resolvedEnums
1321
1573
  });
1574
+ addUsage(usage, response.usage);
1322
1575
  tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1323
1576
  tracer.endSpan(llmSpan);
1324
- run = provenance ? { ...splitProvenance(response.data, schema), issues: [] } : { data: response.data, provenance: {}, issues: [] };
1577
+ run = provenance ? { ...splitProvenance(response.data, schema), issues: [], usage } : { data: response.data, provenance: {}, issues: [], usage };
1578
+ if (hasInput && emptyRetries < retryOnEmpty && isEmptyResult(run.data)) {
1579
+ emptyRetries += 1;
1580
+ tracer.addEvent(rootSpan, "emptyRetry", { retry: emptyRetries });
1581
+ userInput = `${renderedInput}
1582
+
1583
+ ---
1584
+
1585
+ ${EMPTY_RETRY_NOTE}`;
1586
+ attempt -= 1;
1587
+ continue;
1588
+ }
1325
1589
  const validationSpan = tracer.startSpan("validate", { attempt }, rootSpan);
1326
1590
  issues = validate(run.data, schema, bundle, { resolvedEnums });
1327
1591
  tracer.addEvent(validationSpan, "validated", { issueCount: issues.length });
@@ -1347,7 +1611,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1347
1611
  return {
1348
1612
  data: outcome.data,
1349
1613
  provenance: pruneProvenance(run.provenance, outcome.resolved),
1350
- issues: outcome.resolved
1614
+ issues: outcome.resolved,
1615
+ usage
1351
1616
  };
1352
1617
  }
1353
1618
  }
@@ -1368,7 +1633,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1368
1633
  }
1369
1634
  async function prepareSources(sources, options, tracer, parent) {
1370
1635
  const { preprocess, maxInputChars, truncate } = options;
1371
- if (!preprocess && maxInputChars === void 0) {
1636
+ const anyCapped = sources.some((s) => s.maxChars !== void 0);
1637
+ if (!preprocess && maxInputChars === void 0 && !anyCapped) {
1372
1638
  return [...sources];
1373
1639
  }
1374
1640
  const span = tracer.startSpan("prepareInput", {}, parent);
@@ -1385,7 +1651,7 @@ async function prepareSources(sources, options, tracer, parent) {
1385
1651
  lengths: prepared.map((s) => s.text.length)
1386
1652
  });
1387
1653
  }
1388
- if (maxInputChars !== void 0) {
1654
+ if (maxInputChars !== void 0 || prepared.some((s) => s.maxChars !== void 0)) {
1389
1655
  const budgeted = budgetSources(prepared, maxInputChars, truncate);
1390
1656
  prepared = budgeted.sources;
1391
1657
  if (budgeted.truncated.length > 0) {
@@ -1433,19 +1699,59 @@ async function partialCoerce(input, options) {
1433
1699
  });
1434
1700
  return stripNulls(data);
1435
1701
  }
1702
+ async function coerceDetailed(input, options) {
1703
+ const { data, issues, usage } = await runCoercion(input, options, {
1704
+ mode: "coerce",
1705
+ provenance: false
1706
+ });
1707
+ return { data, issues, usage };
1708
+ }
1709
+ async function partialCoerceDetailed(input, options) {
1710
+ const { data, issues, usage } = await runCoercion(input, options, {
1711
+ mode: "partialCoerce",
1712
+ provenance: false
1713
+ });
1714
+ return { data: stripNulls(data), issues, usage };
1715
+ }
1716
+ var PRIME_INPUT = "Cache warm-up. There is no input to extract from; return an object with every field null.";
1717
+ async function primeCache(options) {
1718
+ const { mode = "coerce", provenance = false, ...coerceOptions } = options;
1719
+ const { instructions } = checkOptions(coerceOptions);
1720
+ const tracer = new Tracer(coerceOptions.traceSinks);
1721
+ const rootSpan = tracer.startSpan("primeCache", { schemaId: coerceOptions.schema.id, mode, provenance });
1722
+ const usage = emptyUsage();
1723
+ try {
1724
+ const prepared = await prepareRequest(coerceOptions, { mode, provenance }, instructions, [], tracer, rootSpan);
1725
+ const llmSpan = tracer.startSpan("llmCall", { attempt: 0, warmup: true }, rootSpan);
1726
+ const response = await coerceOptions.provider.complete({
1727
+ systemPrompt: prepared.systemPrompt,
1728
+ userInput: renderSources(toSources(PRIME_INPUT)),
1729
+ jsonSchema: prepared.jsonSchema,
1730
+ schema: prepared.schema,
1731
+ bundle: prepared.bundle,
1732
+ resolvedEnums: prepared.resolvedEnums
1733
+ });
1734
+ addUsage(usage, response.usage);
1735
+ tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1736
+ tracer.endSpan(llmSpan);
1737
+ return { schemaId: coerceOptions.schema.id, mode, provenance, usage, primedAt: (/* @__PURE__ */ new Date()).toISOString() };
1738
+ } finally {
1739
+ tracer.endSpan(rootSpan);
1740
+ }
1741
+ }
1436
1742
  async function coerceWithProvenance(input, options) {
1437
- const { data, provenance, issues } = await runCoercion(input, options, {
1743
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1438
1744
  mode: "coerce",
1439
1745
  provenance: true
1440
1746
  });
1441
- return { data, provenance, issues };
1747
+ return { data, provenance, issues, usage };
1442
1748
  }
1443
1749
  async function partialCoerceWithProvenance(input, options) {
1444
- const { data, provenance, issues } = await runCoercion(input, options, {
1750
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1445
1751
  mode: "partialCoerce",
1446
1752
  provenance: true
1447
1753
  });
1448
- return { data: stripNulls(data), provenance, issues };
1754
+ return { data: stripNulls(data), provenance, issues, usage };
1449
1755
  }
1450
1756
 
1451
1757
  // src/coerce/coerce-many.ts
@@ -1486,60 +1792,107 @@ var BackoffGate = class {
1486
1792
  this.streak = 0;
1487
1793
  }
1488
1794
  };
1795
+ var InputQueue = class {
1796
+ iterator;
1797
+ pulling = Promise.resolve();
1798
+ index = 0;
1799
+ constructor(inputs) {
1800
+ this.iterator = Symbol.asyncIterator in inputs ? inputs[Symbol.asyncIterator]() : inputs[Symbol.iterator]();
1801
+ }
1802
+ next() {
1803
+ const pull = this.pulling.then(async () => {
1804
+ const result = await this.iterator.next();
1805
+ if (result.done) return void 0;
1806
+ return { index: this.index++, input: result.value };
1807
+ });
1808
+ this.pulling = pull.catch(() => void 0);
1809
+ return pull;
1810
+ }
1811
+ };
1812
+ function labelOf(input) {
1813
+ if (typeof input === "string") return void 0;
1814
+ if (isSource(input)) return input.label;
1815
+ return input[0]?.label;
1816
+ }
1489
1817
  async function coerceMany(inputs, options) {
1490
1818
  const {
1491
1819
  concurrency = DEFAULT_CONCURRENCY,
1492
1820
  mode = "coerce",
1493
- provenance = false,
1494
- primeCache = true,
1821
+ provenance: provenanceOption = false,
1822
+ primeCache: primeCache2 = true,
1823
+ primed,
1495
1824
  onItem,
1496
1825
  signal,
1826
+ retry: retryOptions,
1497
1827
  ...coerceOptions
1498
1828
  } = options;
1499
- const retry = { ...DEFAULT_RETRY, ...options.retry };
1829
+ const retry = { ...DEFAULT_RETRY, ...retryOptions };
1830
+ const provenance = provenanceOption !== false;
1831
+ if (Array.isArray(provenanceOption)) {
1832
+ coerceOptions.provenanceFields = provenanceOption;
1833
+ }
1500
1834
  if (!Number.isInteger(concurrency) || concurrency < 1) {
1501
1835
  throw new RangeError(`concurrency must be a positive integer, got ${String(concurrency)}`);
1502
1836
  }
1503
1837
  if (!Number.isInteger(retry.attempts) || retry.attempts < 0) {
1504
1838
  throw new RangeError(`retry.attempts must be a non-negative integer, got ${String(retry.attempts)}`);
1505
1839
  }
1506
- const results = new Array(inputs.length);
1840
+ const results = [];
1507
1841
  const gate = new BackoffGate(retry);
1508
- async function runOne(index) {
1842
+ const queue = new InputQueue(inputs);
1843
+ async function runOne(index, input) {
1509
1844
  let attempts = 0;
1510
1845
  let result;
1846
+ let usage = emptyUsage();
1847
+ const label = labelOf(input);
1848
+ const traceAttributes = { itemIndex: index, ...label !== void 0 ? { itemLabel: label } : {} };
1511
1849
  for (; ; ) {
1512
1850
  if (signal?.aborted) {
1513
- result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), attempts };
1851
+ result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), usage, attempts };
1514
1852
  break;
1515
1853
  }
1516
1854
  await gate.wait();
1517
1855
  attempts += 1;
1518
1856
  try {
1519
- const run = await runCoercion(inputs[index], coerceOptions, { mode, provenance });
1857
+ const run = await runCoercion(input, coerceOptions, { mode, provenance, traceAttributes });
1520
1858
  const data = mode === "partialCoerce" ? stripNulls(run.data) : run.data;
1521
1859
  gate.succeeded();
1522
- result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, attempts };
1860
+ result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, usage: run.usage, attempts };
1523
1861
  break;
1524
1862
  } catch (error) {
1863
+ usage = emptyUsage();
1525
1864
  if (isRetryable(error) && attempts <= retry.attempts) {
1526
1865
  gate.failed();
1527
1866
  continue;
1528
1867
  }
1529
- result = { ok: false, index, error, attempts };
1868
+ result = { ok: false, index, error, usage, attempts };
1530
1869
  break;
1531
1870
  }
1532
1871
  }
1533
1872
  results[index] = result;
1534
1873
  onItem?.(result);
1535
1874
  }
1536
- let next = 0;
1537
- if (primeCache && inputs.length > 1) {
1538
- await runOne(next++);
1875
+ let pending;
1876
+ const warmup = primed ?? (primeCache2 === "eager" ? primeCache({ ...coerceOptions, mode, provenance }) : void 0);
1877
+ if (warmup) {
1878
+ await Promise.resolve(warmup).catch(() => void 0);
1879
+ } else if (primeCache2 === true) {
1880
+ const first = await queue.next();
1881
+ if (first === void 0) return results;
1882
+ const second = await queue.next();
1883
+ if (second === void 0) {
1884
+ await runOne(first.index, first.input);
1885
+ return results;
1886
+ }
1887
+ await runOne(first.index, first.input);
1888
+ pending = second;
1539
1889
  }
1540
- const workers = Array.from({ length: Math.min(concurrency, inputs.length) }, async () => {
1541
- while (next < inputs.length) {
1542
- await runOne(next++);
1890
+ const workers = Array.from({ length: concurrency }, async () => {
1891
+ for (; ; ) {
1892
+ const item = pending ?? await queue.next();
1893
+ pending = void 0;
1894
+ if (item === void 0) return;
1895
+ await runOne(item.index, item.input);
1543
1896
  }
1544
1897
  });
1545
1898
  await Promise.all(workers);
@@ -1578,6 +1931,7 @@ function resolveConfig(callConfig) {
1578
1931
  maxRepairAttempts: callConfig?.maxRepairAttempts ?? global.maxRepairAttempts,
1579
1932
  onInvalidField: callConfig?.onInvalidField ?? global.onInvalidField,
1580
1933
  instructions: callConfig?.instructions ?? global.instructions,
1934
+ retryOnEmpty: callConfig?.retryOnEmpty ?? global.retryOnEmpty,
1581
1935
  maxInputChars: callConfig?.maxInputChars ?? global.maxInputChars,
1582
1936
  truncate: callConfig?.truncate ?? global.truncate,
1583
1937
  preprocess: callConfig?.preprocess ?? global.preprocess
@@ -1612,6 +1966,7 @@ var Coercible = class _Coercible {
1612
1966
  maxRepairAttempts: this._config.maxRepairAttempts,
1613
1967
  onInvalidField: this._config.onInvalidField,
1614
1968
  instructions: this._config.instructions,
1969
+ retryOnEmpty: this._config.retryOnEmpty,
1615
1970
  maxInputChars: this._config.maxInputChars,
1616
1971
  truncate: this._config.truncate,
1617
1972
  preprocess: this._config.preprocess
@@ -1677,6 +2032,7 @@ export {
1677
2032
  Constrain,
1678
2033
  Describe,
1679
2034
  EnumResolutionError,
2035
+ FIELD_FORMATS,
1680
2036
  PROVENANCE_INSTRUCTIONS,
1681
2037
  SOURCE_INSTRUCTIONS,
1682
2038
  Schema,
@@ -1689,16 +2045,21 @@ export {
1689
2045
  buildRepairInput,
1690
2046
  bundleOf,
1691
2047
  coerce,
2048
+ coerceDetailed,
1692
2049
  coerceMany,
1693
2050
  coerceWithProvenance,
1694
2051
  collectEnumSources,
1695
2052
  defineSchema,
2053
+ describeFormat,
1696
2054
  field,
2055
+ formatToJsonSchema,
1697
2056
  isCoerceInput,
1698
2057
  isSource,
1699
2058
  normalizeInstructions,
1700
2059
  partialCoerce,
2060
+ partialCoerceDetailed,
1701
2061
  partialCoerceWithProvenance,
2062
+ primeCache,
1702
2063
  provenanceInstructions,
1703
2064
  renderSources,
1704
2065
  resolveEnumSources,
@@ -1709,6 +2070,7 @@ export {
1709
2070
  toOpenAIJsonSchema,
1710
2071
  toProvenanceSchema,
1711
2072
  toSources,
2073
+ validateFormat,
1712
2074
  validatePartial,
1713
2075
  validateStrict
1714
2076
  };