@sembl/core 0.2.1 → 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.cjs CHANGED
@@ -26,6 +26,7 @@ __export(index_exports, {
26
26
  Constrain: () => Constrain,
27
27
  Describe: () => Describe,
28
28
  EnumResolutionError: () => EnumResolutionError,
29
+ FIELD_FORMATS: () => FIELD_FORMATS,
29
30
  PROVENANCE_INSTRUCTIONS: () => PROVENANCE_INSTRUCTIONS,
30
31
  SOURCE_INSTRUCTIONS: () => SOURCE_INSTRUCTIONS,
31
32
  Schema: () => Schema,
@@ -38,15 +39,21 @@ __export(index_exports, {
38
39
  buildRepairInput: () => buildRepairInput,
39
40
  bundleOf: () => bundleOf,
40
41
  coerce: () => coerce,
42
+ coerceDetailed: () => coerceDetailed,
41
43
  coerceMany: () => coerceMany,
42
44
  coerceWithProvenance: () => coerceWithProvenance,
43
45
  collectEnumSources: () => collectEnumSources,
44
46
  defineSchema: () => defineSchema,
47
+ describeFormat: () => describeFormat,
45
48
  field: () => field,
49
+ formatToJsonSchema: () => formatToJsonSchema,
46
50
  isCoerceInput: () => isCoerceInput,
47
51
  isSource: () => isSource,
52
+ normalizeInstructions: () => normalizeInstructions,
48
53
  partialCoerce: () => partialCoerce,
54
+ partialCoerceDetailed: () => partialCoerceDetailed,
49
55
  partialCoerceWithProvenance: () => partialCoerceWithProvenance,
56
+ primeCache: () => primeCache,
50
57
  provenanceInstructions: () => provenanceInstructions,
51
58
  renderSources: () => renderSources,
52
59
  resolveEnumSources: () => resolveEnumSources,
@@ -57,11 +64,164 @@ __export(index_exports, {
57
64
  toOpenAIJsonSchema: () => toOpenAIJsonSchema,
58
65
  toProvenanceSchema: () => toProvenanceSchema,
59
66
  toSources: () => toSources,
67
+ validateFormat: () => validateFormat,
60
68
  validatePartial: () => validatePartial,
61
69
  validateStrict: () => validateStrict
62
70
  });
63
71
  module.exports = __toCommonJS(index_exports);
64
72
 
73
+ // src/schema/formats.ts
74
+ var FIELD_FORMATS = [
75
+ "url",
76
+ "email",
77
+ "date",
78
+ "datetime",
79
+ "iso-country",
80
+ "us-state",
81
+ "us-state-name",
82
+ "currency"
83
+ ];
84
+ var ISO_COUNTRIES = new Set(
85
+ "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(" ")
86
+ );
87
+ var US_STATES = {
88
+ AL: "Alabama",
89
+ AK: "Alaska",
90
+ AZ: "Arizona",
91
+ AR: "Arkansas",
92
+ CA: "California",
93
+ CO: "Colorado",
94
+ CT: "Connecticut",
95
+ DE: "Delaware",
96
+ FL: "Florida",
97
+ GA: "Georgia",
98
+ HI: "Hawaii",
99
+ ID: "Idaho",
100
+ IL: "Illinois",
101
+ IN: "Indiana",
102
+ IA: "Iowa",
103
+ KS: "Kansas",
104
+ KY: "Kentucky",
105
+ LA: "Louisiana",
106
+ ME: "Maine",
107
+ MD: "Maryland",
108
+ MA: "Massachusetts",
109
+ MI: "Michigan",
110
+ MN: "Minnesota",
111
+ MS: "Mississippi",
112
+ MO: "Missouri",
113
+ MT: "Montana",
114
+ NE: "Nebraska",
115
+ NV: "Nevada",
116
+ NH: "New Hampshire",
117
+ NJ: "New Jersey",
118
+ NM: "New Mexico",
119
+ NY: "New York",
120
+ NC: "North Carolina",
121
+ ND: "North Dakota",
122
+ OH: "Ohio",
123
+ OK: "Oklahoma",
124
+ OR: "Oregon",
125
+ PA: "Pennsylvania",
126
+ RI: "Rhode Island",
127
+ SC: "South Carolina",
128
+ SD: "South Dakota",
129
+ TN: "Tennessee",
130
+ TX: "Texas",
131
+ UT: "Utah",
132
+ VT: "Vermont",
133
+ VA: "Virginia",
134
+ WA: "Washington",
135
+ WV: "West Virginia",
136
+ WI: "Wisconsin",
137
+ WY: "Wyoming",
138
+ DC: "District of Columbia",
139
+ PR: "Puerto Rico",
140
+ GU: "Guam",
141
+ VI: "U.S. Virgin Islands",
142
+ AS: "American Samoa",
143
+ MP: "Northern Mariana Islands"
144
+ };
145
+ var US_STATE_NAMES = new Set(Object.values(US_STATES));
146
+ var CURRENCIES = new Set(
147
+ "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(" ")
148
+ );
149
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
150
+ var DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
151
+ var DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
152
+ function isCalendarDate(y, m, d) {
153
+ const date = new Date(Date.UTC(y, m - 1, d));
154
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
155
+ }
156
+ function validateFormat(value, format) {
157
+ switch (format) {
158
+ case "url": {
159
+ try {
160
+ const url = new URL(value);
161
+ if (url.protocol === "http:" || url.protocol === "https:") return void 0;
162
+ } catch {
163
+ }
164
+ return `Expected an absolute http(s) URL, got ${JSON.stringify(value)}`;
165
+ }
166
+ case "email":
167
+ return EMAIL.test(value) ? void 0 : `Expected an email address, got ${JSON.stringify(value)}`;
168
+ case "date": {
169
+ const m = DATE.exec(value);
170
+ if (m && isCalendarDate(Number(m[1]), Number(m[2]), Number(m[3]))) return void 0;
171
+ return `Expected a calendar date as YYYY-MM-DD, got ${JSON.stringify(value)}`;
172
+ }
173
+ case "datetime":
174
+ return DATETIME.test(value) && !Number.isNaN(Date.parse(value)) ? void 0 : `Expected an ISO 8601 timestamp, got ${JSON.stringify(value)}`;
175
+ case "iso-country":
176
+ 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)}`;
177
+ case "us-state":
178
+ return value in US_STATES ? void 0 : `Expected a two-letter USPS state code such as CA or NY, got ${JSON.stringify(value)}`;
179
+ case "us-state-name":
180
+ return US_STATE_NAMES.has(value) ? void 0 : `Expected a US state's full name such as California, got ${JSON.stringify(value)}`;
181
+ case "currency":
182
+ return CURRENCIES.has(value) ? void 0 : `Expected an ISO 4217 currency code such as USD or EUR, got ${JSON.stringify(value)}`;
183
+ }
184
+ }
185
+ function describeFormat(format) {
186
+ switch (format) {
187
+ case "url":
188
+ return "an absolute http(s) URL";
189
+ case "email":
190
+ return "an email address";
191
+ case "date":
192
+ return "a calendar date as YYYY-MM-DD";
193
+ case "datetime":
194
+ return "an ISO 8601 timestamp (e.g. 2026-09-05T14:30:00Z)";
195
+ case "iso-country":
196
+ return "an ISO 3166-1 alpha-2 country code (e.g. US, DE, PT), never a country name";
197
+ case "us-state":
198
+ return "a two-letter USPS state code (e.g. CA, NY), never the state's name";
199
+ case "us-state-name":
200
+ return "a US state's full name (e.g. California, New York), never its abbreviation";
201
+ case "currency":
202
+ return "an ISO 4217 currency code (e.g. USD, EUR, GBP), never a symbol or a word";
203
+ }
204
+ }
205
+ function formatToJsonSchema(format) {
206
+ switch (format) {
207
+ case "url":
208
+ return { format: "uri" };
209
+ case "email":
210
+ return { format: "email" };
211
+ case "date":
212
+ return { format: "date" };
213
+ case "datetime":
214
+ return { format: "date-time" };
215
+ case "iso-country":
216
+ case "us-state":
217
+ return { pattern: "^[A-Z]{2}$" };
218
+ case "currency":
219
+ return { pattern: "^[A-Z]{3}$" };
220
+ case "us-state-name":
221
+ return {};
222
+ }
223
+ }
224
+
65
225
  // src/schema/json-schema.ts
66
226
  var CONSTRAINT_KEYWORDS = [
67
227
  "maxLength",
@@ -78,6 +238,9 @@ function constraintsToJsonSchema(constraints, dialect) {
78
238
  return {};
79
239
  }
80
240
  const out = {};
241
+ if (constraints.format !== void 0) {
242
+ Object.assign(out, formatToJsonSchema(constraints.format));
243
+ }
81
244
  for (const keyword of CONSTRAINT_KEYWORDS) {
82
245
  const value = constraints[keyword];
83
246
  if (value !== void 0) {
@@ -462,7 +625,7 @@ ${summary}`);
462
625
  // src/coerce/sources.ts
463
626
  var SOURCE_TAG = "source";
464
627
  function isSource(value) {
465
- return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.text === "string" && (value.label === void 0 || typeof value.label === "string");
628
+ 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");
466
629
  }
467
630
  function isCoerceInput(value) {
468
631
  return typeof value === "string" || isSource(value) || Array.isArray(value) && value.every(isSource);
@@ -482,7 +645,9 @@ function toSources(input) {
482
645
  }
483
646
  function cleanLabel(source) {
484
647
  const label = source.label?.trim();
485
- return label ? { label, text: source.text } : { text: source.text };
648
+ const cleaned = label ? { label, text: source.text } : { text: source.text };
649
+ if (source.maxChars !== void 0) cleaned.maxChars = source.maxChars;
650
+ return cleaned;
486
651
  }
487
652
  function escapeText(text) {
488
653
  return text.replace(new RegExp(`</(\\s*${SOURCE_TAG}\\b)`, "gi"), "<\\/$1");
@@ -507,12 +672,23 @@ var SOURCE_INSTRUCTIONS = [
507
672
  ].join("\n");
508
673
 
509
674
  // src/coerce/prompt-builder.ts
675
+ function normalizeInstructions(instructions) {
676
+ if (instructions === void 0) return [];
677
+ const list = typeof instructions === "string" ? [instructions] : instructions;
678
+ if (!Array.isArray(list) || list.some((entry) => typeof entry !== "string")) {
679
+ throw new RangeError("instructions must be a string or an array of strings");
680
+ }
681
+ return list.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
682
+ }
510
683
  function describeConstraints(constraints) {
511
684
  if (!constraints) {
512
685
  return [];
513
686
  }
514
687
  const phrases = [];
515
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
688
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
689
+ if (format !== void 0) {
690
+ phrases.push(describeFormat(format));
691
+ }
516
692
  if (minLength !== void 0 && maxLength !== void 0) {
517
693
  phrases.push(`between ${minLength} and ${maxLength} characters`);
518
694
  } else if (maxLength !== void 0) {
@@ -625,10 +801,20 @@ function buildPrompt(schema, bundle, options = {}) {
625
801
  lines.push("Instructions:");
626
802
  lines.push("- Extract values from the sources that match the schema fields.");
627
803
  lines.push("- Use null for optional fields that cannot be determined from the input.");
804
+ 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.");
805
+ lines.push("- Never return an empty object when the input states values for any field.");
628
806
  lines.push("- Required fields must always have a valid, non-null value.");
629
807
  lines.push("- Interpret the user's input semantically \u2014 infer meaning, don't just pattern match.");
630
808
  lines.push("- Respect every stated limit exactly; truncate or drop lower-priority content to stay within it.");
631
809
  lines.push("- Return only the structured JSON output matching the schema.");
810
+ const instructions = normalizeInstructions(options.instructions);
811
+ if (instructions.length > 0) {
812
+ lines.push("");
813
+ lines.push("Additional guidance for this extraction:");
814
+ for (const instruction of instructions) {
815
+ lines.push(`- ${instruction}`);
816
+ }
817
+ }
632
818
  return lines.join("\n");
633
819
  }
634
820
 
@@ -682,11 +868,31 @@ var PROVENANCE_INSTRUCTIONS = [
682
868
  "- Judge each field on its own. A confident value next to a guessed one is",
683
869
  " normal, and marking the guess honestly is more useful than looking sure."
684
870
  ].join("\n");
871
+ function provenanceFieldNames(schema, fields) {
872
+ if (fields === void 0) return new Set(schema.fields.map((f) => f.name));
873
+ const known = new Set(schema.fields.map((f) => f.name));
874
+ for (const name of fields) {
875
+ if (!known.has(name)) {
876
+ throw new RangeError(
877
+ `provenance field "${name}" is not a field of schema "${schema.id}" (fields: ${[...known].join(", ")})`
878
+ );
879
+ }
880
+ }
881
+ return new Set(fields);
882
+ }
685
883
  function provenanceInstructions(options = {}) {
686
884
  const labels = options.sourceLabels ?? [];
687
- if (labels.length < 2) return PROVENANCE_INSTRUCTIONS;
688
- return `${PROVENANCE_INSTRUCTIONS}
689
- - Set \`source\` to the label of the source the value was read from.`;
885
+ let text = PROVENANCE_INSTRUCTIONS;
886
+ if (options.fields !== void 0) {
887
+ text = text.replace(
888
+ "- Every field is wrapped as an object: put the extracted value in `value`.",
889
+ `- Only these fields are wrapped as objects, with the extracted value in \`value\`: ${options.fields.join(", ")}. Every other field is a plain value.`
890
+ );
891
+ }
892
+ if (labels.length >= 2) {
893
+ 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`.";
894
+ }
895
+ return text;
690
896
  }
691
897
  function annotationSchema(parentId, field2, sourceLabels) {
692
898
  const valueField = {
@@ -718,7 +924,7 @@ function annotationSchema(parentId, field2, sourceLabels) {
718
924
  name: "source",
719
925
  description: "The label of the source this value was read from.",
720
926
  type: { kind: "enum", values: [...sourceLabels] },
721
- required: false
927
+ required: true
722
928
  }
723
929
  ] : []
724
930
  ]
@@ -728,7 +934,12 @@ function toProvenanceSchema(schema, bundle, options = {}) {
728
934
  const schemas = { ...bundle?.schemas ?? {} };
729
935
  const fields = [];
730
936
  const sourceLabels = options.sourceLabels ?? [];
937
+ const wrapped = provenanceFieldNames(schema, options.fields);
731
938
  for (const field2 of schema.fields) {
939
+ if (!wrapped.has(field2.name)) {
940
+ fields.push(field2);
941
+ continue;
942
+ }
732
943
  const annotation = annotationSchema(schema.id, field2, sourceLabels);
733
944
  schemas[annotation.id] = annotation;
734
945
  fields.push({
@@ -804,10 +1015,30 @@ ${tailRoom > 0 ? text.slice(text.length - tailRoom) : ""}`;
804
1015
  }
805
1016
  }
806
1017
  function budgetSources(sources, maxChars, policy = "tail") {
807
- const total = sources.reduce((sum, s) => sum + s.text.length, 0);
808
- if (total <= maxChars) {
809
- return { sources: [...sources], truncated: [] };
1018
+ const records = /* @__PURE__ */ new Map();
1019
+ const record = (index, source, text) => {
1020
+ const existing = records.get(index);
1021
+ if (existing) {
1022
+ existing.keptLength = text.length;
1023
+ } else {
1024
+ records.set(index, {
1025
+ ...source.label !== void 0 ? { label: source.label } : {},
1026
+ originalLength: source.text.length,
1027
+ keptLength: text.length
1028
+ });
1029
+ }
1030
+ };
1031
+ const capped = sources.map((source, index) => {
1032
+ if (source.maxChars === void 0 || source.text.length <= source.maxChars) return source;
1033
+ const text = truncateText(source.text, source.maxChars, policy);
1034
+ record(index, source, text);
1035
+ return { ...source, text };
1036
+ });
1037
+ const total = capped.reduce((sum, s) => sum + s.text.length, 0);
1038
+ if (maxChars === void 0 || total <= maxChars) {
1039
+ return { sources: capped, truncated: [...records.values()] };
810
1040
  }
1041
+ sources = capped;
811
1042
  const allowance = /* @__PURE__ */ new Map();
812
1043
  const order = sources.map((s, i) => i).sort((a, b) => sources[a].text.length - sources[b].text.length);
813
1044
  let remaining = maxChars;
@@ -817,19 +1048,14 @@ function budgetSources(sources, maxChars, policy = "tail") {
817
1048
  allowance.set(index, granted);
818
1049
  remaining -= granted;
819
1050
  });
820
- const truncated = [];
821
1051
  const budgeted = sources.map((source, index) => {
822
1052
  const limit = allowance.get(index) ?? 0;
823
1053
  if (source.text.length <= limit) return source;
824
1054
  const text = truncateText(source.text, limit, policy);
825
- truncated.push({
826
- ...source.label !== void 0 ? { label: source.label } : {},
827
- originalLength: source.text.length,
828
- keptLength: text.length
829
- });
1055
+ record(index, source, text);
830
1056
  return { ...source, text };
831
1057
  });
832
- return { sources: budgeted, truncated };
1058
+ return { sources: budgeted, truncated: [...records.values()] };
833
1059
  }
834
1060
 
835
1061
  // src/coerce/validator.ts
@@ -845,7 +1071,7 @@ function entries(count) {
845
1071
  return `${count} ${count === 1 ? "entry" : "entries"}`;
846
1072
  }
847
1073
  function validateConstraints(value, constraints, path, issues) {
848
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
1074
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
849
1075
  if (Array.isArray(value)) {
850
1076
  if (minItems !== void 0 && value.length < minItems) {
851
1077
  issues.push({
@@ -889,6 +1115,10 @@ function validateConstraints(value, constraints, path, issues) {
889
1115
  received: value
890
1116
  });
891
1117
  }
1118
+ if (format !== void 0) {
1119
+ const message = validateFormat(value, format);
1120
+ if (message) issues.push({ path, message, received: value });
1121
+ }
892
1122
  return;
893
1123
  }
894
1124
  if (typeof value === "number") {
@@ -1230,8 +1460,15 @@ function generateSpanId() {
1230
1460
  }
1231
1461
  var Tracer = class {
1232
1462
  sinks;
1233
- constructor(sinks) {
1463
+ baseAttributes;
1464
+ /**
1465
+ * `baseAttributes` are merged into every span this tracer opens — how a
1466
+ * batch stamps `itemIndex` on the spans of each item, so a sink can tell
1467
+ * whose `llmCall` it is looking at under concurrency.
1468
+ */
1469
+ constructor(sinks, baseAttributes) {
1234
1470
  this.sinks = sinks ?? [];
1471
+ this.baseAttributes = baseAttributes;
1235
1472
  }
1236
1473
  startSpan(name, attributes, parent) {
1237
1474
  return {
@@ -1239,7 +1476,7 @@ var Tracer = class {
1239
1476
  name,
1240
1477
  startTime: Date.now(),
1241
1478
  events: [],
1242
- attributes,
1479
+ attributes: this.baseAttributes ? { ...this.baseAttributes, ...attributes } : attributes,
1243
1480
  parentId: parent?.id
1244
1481
  };
1245
1482
  }
@@ -1294,15 +1531,32 @@ async function resolveEnums(schema, bundle, enumResolver, tracer, parent) {
1294
1531
  tracer.endSpan(span);
1295
1532
  }
1296
1533
  }
1297
- async function runCoercion(input, options, { mode, provenance }) {
1298
- const { provider, schema, enumResolver, traceSinks } = options;
1299
- const bundle = options.bundle ?? bundleOf(schema);
1534
+ function emptyUsage() {
1535
+ return { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
1536
+ }
1537
+ function addUsage(into, usage) {
1538
+ into.calls += 1;
1539
+ if (!usage) return;
1540
+ into.promptTokens += usage.promptTokens;
1541
+ into.completionTokens += usage.completionTokens;
1542
+ into.totalTokens += usage.totalTokens;
1543
+ into.cacheReadTokens += usage.cacheReadTokens ?? 0;
1544
+ into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
1545
+ }
1546
+ function checkOptions(options) {
1300
1547
  const maxRepairAttempts = options.maxRepairAttempts ?? 0;
1301
1548
  if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
1302
1549
  throw new RangeError(
1303
1550
  `maxRepairAttempts must be a non-negative integer, got ${String(options.maxRepairAttempts)}`
1304
1551
  );
1305
1552
  }
1553
+ const retryOnEmpty = options.retryOnEmpty ?? 0;
1554
+ if (!Number.isInteger(retryOnEmpty) || retryOnEmpty < 0) {
1555
+ throw new RangeError(
1556
+ `retryOnEmpty must be a non-negative integer, got ${String(options.retryOnEmpty)}`
1557
+ );
1558
+ }
1559
+ const instructions = normalizeInstructions(options.instructions);
1306
1560
  const onInvalidField = options.onInvalidField ?? "throw";
1307
1561
  if (!INVALID_FIELD_POLICIES.includes(onInvalidField)) {
1308
1562
  throw new RangeError(
@@ -1314,60 +1568,96 @@ async function runCoercion(input, options, { mode, provenance }) {
1314
1568
  `maxInputChars must be a positive integer, got ${String(options.maxInputChars)}`
1315
1569
  );
1316
1570
  }
1571
+ return { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions };
1572
+ }
1573
+ async function prepareRequest(options, { mode, provenance }, instructions, sourceLabels, tracer, rootSpan) {
1574
+ const { schema, enumResolver } = options;
1575
+ const bundle = options.bundle ?? bundleOf(schema);
1576
+ const resolvedEnums = await resolveEnums(schema, bundle, enumResolver, tracer, rootSpan);
1577
+ const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1578
+ const basePrompt = buildPrompt(schema, bundle, { resolvedEnums, instructions });
1579
+ const provenanceOptions = { sourceLabels, fields: options.provenanceFields };
1580
+ const systemPrompt = provenance ? `${basePrompt}
1581
+ ${provenanceInstructions(provenanceOptions)}` : basePrompt;
1582
+ tracer.addEvent(promptSpan, "promptBuilt", {
1583
+ promptLength: systemPrompt.length,
1584
+ instructionCount: instructions.length,
1585
+ mode
1586
+ });
1587
+ tracer.endSpan(promptSpan);
1588
+ const request = provenance ? toProvenanceSchema(schema, bundle, provenanceOptions) : { schema, bundle };
1589
+ const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1590
+ const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1591
+ resolvedEnums
1592
+ });
1593
+ tracer.endSpan(schemaSpan);
1594
+ return { systemPrompt, jsonSchema, schema: request.schema, bundle: request.bundle, resolvedEnums };
1595
+ }
1596
+ function isEmptyResult(data) {
1597
+ return Object.values(data).every((value) => value === null || value === void 0);
1598
+ }
1599
+ 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.";
1600
+ async function runCoercion(input, options, { mode, provenance, traceAttributes }) {
1601
+ const { provider, schema, traceSinks } = options;
1602
+ const bundle = options.bundle ?? bundleOf(schema);
1603
+ const { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions } = checkOptions(options);
1317
1604
  const rawSources = toSources(input);
1318
- const tracer = new Tracer(traceSinks);
1605
+ const tracer = new Tracer(traceSinks, traceAttributes);
1319
1606
  const rootSpan = tracer.startSpan(mode, {
1320
1607
  schemaId: schema.id,
1321
1608
  provenance,
1322
1609
  onInvalidField,
1323
1610
  sourceCount: rawSources.length
1324
1611
  });
1612
+ const usage = emptyUsage();
1325
1613
  try {
1326
1614
  const sources = await prepareSources(rawSources, options, tracer, rootSpan);
1327
1615
  const sourceLabels = sources.length > 1 ? sources.map((s) => s.label ?? "") : [];
1328
- const resolvedEnums = await resolveEnums(
1329
- schema,
1330
- bundle,
1331
- enumResolver,
1616
+ const prepared = await prepareRequest(
1617
+ options,
1618
+ { mode, provenance },
1619
+ instructions,
1620
+ sourceLabels,
1332
1621
  tracer,
1333
1622
  rootSpan
1334
1623
  );
1335
- const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1336
- const basePrompt = buildPrompt(schema, bundle, { resolvedEnums });
1337
- const systemPrompt = provenance ? `${basePrompt}
1338
- ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1339
- tracer.addEvent(promptSpan, "promptBuilt", {
1340
- promptLength: systemPrompt.length
1341
- });
1342
- tracer.endSpan(promptSpan);
1343
- const request = provenance ? toProvenanceSchema(schema, bundle, { sourceLabels }) : { schema, bundle };
1344
- const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1345
- const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1346
- resolvedEnums
1347
- });
1348
- tracer.endSpan(schemaSpan);
1624
+ const { systemPrompt, jsonSchema, resolvedEnums } = prepared;
1349
1625
  const validate = mode === "coerce" ? validateStrict : validatePartial;
1350
1626
  const renderedInput = renderSources(sources);
1627
+ const hasInput = sources.some((s) => s.text.trim().length > 0);
1351
1628
  tracer.addEvent(rootSpan, "inputRendered", {
1352
1629
  sourceCount: sources.length,
1353
1630
  inputLength: renderedInput.length
1354
1631
  });
1355
1632
  let userInput = renderedInput;
1356
1633
  let issues = [];
1357
- let run = { data: {}, provenance: {}, issues: [] };
1634
+ let run = { data: {}, provenance: {}, issues: [], usage };
1635
+ let emptyRetries = 0;
1358
1636
  for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
1359
1637
  const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
1360
1638
  const response = await provider.complete({
1361
1639
  systemPrompt,
1362
1640
  userInput,
1363
1641
  jsonSchema,
1364
- schema: request.schema,
1365
- bundle: request.bundle,
1642
+ schema: prepared.schema,
1643
+ bundle: prepared.bundle,
1366
1644
  resolvedEnums
1367
1645
  });
1646
+ addUsage(usage, response.usage);
1368
1647
  tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1369
1648
  tracer.endSpan(llmSpan);
1370
- run = provenance ? { ...splitProvenance(response.data, schema), issues: [] } : { data: response.data, provenance: {}, issues: [] };
1649
+ run = provenance ? { ...splitProvenance(response.data, schema), issues: [], usage } : { data: response.data, provenance: {}, issues: [], usage };
1650
+ if (hasInput && emptyRetries < retryOnEmpty && isEmptyResult(run.data)) {
1651
+ emptyRetries += 1;
1652
+ tracer.addEvent(rootSpan, "emptyRetry", { retry: emptyRetries });
1653
+ userInput = `${renderedInput}
1654
+
1655
+ ---
1656
+
1657
+ ${EMPTY_RETRY_NOTE}`;
1658
+ attempt -= 1;
1659
+ continue;
1660
+ }
1371
1661
  const validationSpan = tracer.startSpan("validate", { attempt }, rootSpan);
1372
1662
  issues = validate(run.data, schema, bundle, { resolvedEnums });
1373
1663
  tracer.addEvent(validationSpan, "validated", { issueCount: issues.length });
@@ -1393,7 +1683,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1393
1683
  return {
1394
1684
  data: outcome.data,
1395
1685
  provenance: pruneProvenance(run.provenance, outcome.resolved),
1396
- issues: outcome.resolved
1686
+ issues: outcome.resolved,
1687
+ usage
1397
1688
  };
1398
1689
  }
1399
1690
  }
@@ -1414,7 +1705,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1414
1705
  }
1415
1706
  async function prepareSources(sources, options, tracer, parent) {
1416
1707
  const { preprocess, maxInputChars, truncate } = options;
1417
- if (!preprocess && maxInputChars === void 0) {
1708
+ const anyCapped = sources.some((s) => s.maxChars !== void 0);
1709
+ if (!preprocess && maxInputChars === void 0 && !anyCapped) {
1418
1710
  return [...sources];
1419
1711
  }
1420
1712
  const span = tracer.startSpan("prepareInput", {}, parent);
@@ -1431,7 +1723,7 @@ async function prepareSources(sources, options, tracer, parent) {
1431
1723
  lengths: prepared.map((s) => s.text.length)
1432
1724
  });
1433
1725
  }
1434
- if (maxInputChars !== void 0) {
1726
+ if (maxInputChars !== void 0 || prepared.some((s) => s.maxChars !== void 0)) {
1435
1727
  const budgeted = budgetSources(prepared, maxInputChars, truncate);
1436
1728
  prepared = budgeted.sources;
1437
1729
  if (budgeted.truncated.length > 0) {
@@ -1479,19 +1771,59 @@ async function partialCoerce(input, options) {
1479
1771
  });
1480
1772
  return stripNulls(data);
1481
1773
  }
1774
+ async function coerceDetailed(input, options) {
1775
+ const { data, issues, usage } = await runCoercion(input, options, {
1776
+ mode: "coerce",
1777
+ provenance: false
1778
+ });
1779
+ return { data, issues, usage };
1780
+ }
1781
+ async function partialCoerceDetailed(input, options) {
1782
+ const { data, issues, usage } = await runCoercion(input, options, {
1783
+ mode: "partialCoerce",
1784
+ provenance: false
1785
+ });
1786
+ return { data: stripNulls(data), issues, usage };
1787
+ }
1788
+ var PRIME_INPUT = "Cache warm-up. There is no input to extract from; return an object with every field null.";
1789
+ async function primeCache(options) {
1790
+ const { mode = "coerce", provenance = false, ...coerceOptions } = options;
1791
+ const { instructions } = checkOptions(coerceOptions);
1792
+ const tracer = new Tracer(coerceOptions.traceSinks);
1793
+ const rootSpan = tracer.startSpan("primeCache", { schemaId: coerceOptions.schema.id, mode, provenance });
1794
+ const usage = emptyUsage();
1795
+ try {
1796
+ const prepared = await prepareRequest(coerceOptions, { mode, provenance }, instructions, [], tracer, rootSpan);
1797
+ const llmSpan = tracer.startSpan("llmCall", { attempt: 0, warmup: true }, rootSpan);
1798
+ const response = await coerceOptions.provider.complete({
1799
+ systemPrompt: prepared.systemPrompt,
1800
+ userInput: renderSources(toSources(PRIME_INPUT)),
1801
+ jsonSchema: prepared.jsonSchema,
1802
+ schema: prepared.schema,
1803
+ bundle: prepared.bundle,
1804
+ resolvedEnums: prepared.resolvedEnums
1805
+ });
1806
+ addUsage(usage, response.usage);
1807
+ tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1808
+ tracer.endSpan(llmSpan);
1809
+ return { schemaId: coerceOptions.schema.id, mode, provenance, usage, primedAt: (/* @__PURE__ */ new Date()).toISOString() };
1810
+ } finally {
1811
+ tracer.endSpan(rootSpan);
1812
+ }
1813
+ }
1482
1814
  async function coerceWithProvenance(input, options) {
1483
- const { data, provenance, issues } = await runCoercion(input, options, {
1815
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1484
1816
  mode: "coerce",
1485
1817
  provenance: true
1486
1818
  });
1487
- return { data, provenance, issues };
1819
+ return { data, provenance, issues, usage };
1488
1820
  }
1489
1821
  async function partialCoerceWithProvenance(input, options) {
1490
- const { data, provenance, issues } = await runCoercion(input, options, {
1822
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1491
1823
  mode: "partialCoerce",
1492
1824
  provenance: true
1493
1825
  });
1494
- return { data: stripNulls(data), provenance, issues };
1826
+ return { data: stripNulls(data), provenance, issues, usage };
1495
1827
  }
1496
1828
 
1497
1829
  // src/coerce/coerce-many.ts
@@ -1532,60 +1864,107 @@ var BackoffGate = class {
1532
1864
  this.streak = 0;
1533
1865
  }
1534
1866
  };
1867
+ var InputQueue = class {
1868
+ iterator;
1869
+ pulling = Promise.resolve();
1870
+ index = 0;
1871
+ constructor(inputs) {
1872
+ this.iterator = Symbol.asyncIterator in inputs ? inputs[Symbol.asyncIterator]() : inputs[Symbol.iterator]();
1873
+ }
1874
+ next() {
1875
+ const pull = this.pulling.then(async () => {
1876
+ const result = await this.iterator.next();
1877
+ if (result.done) return void 0;
1878
+ return { index: this.index++, input: result.value };
1879
+ });
1880
+ this.pulling = pull.catch(() => void 0);
1881
+ return pull;
1882
+ }
1883
+ };
1884
+ function labelOf(input) {
1885
+ if (typeof input === "string") return void 0;
1886
+ if (isSource(input)) return input.label;
1887
+ return input[0]?.label;
1888
+ }
1535
1889
  async function coerceMany(inputs, options) {
1536
1890
  const {
1537
1891
  concurrency = DEFAULT_CONCURRENCY,
1538
1892
  mode = "coerce",
1539
- provenance = false,
1540
- primeCache = true,
1893
+ provenance: provenanceOption = false,
1894
+ primeCache: primeCache2 = true,
1895
+ primed,
1541
1896
  onItem,
1542
1897
  signal,
1898
+ retry: retryOptions,
1543
1899
  ...coerceOptions
1544
1900
  } = options;
1545
- const retry = { ...DEFAULT_RETRY, ...options.retry };
1901
+ const retry = { ...DEFAULT_RETRY, ...retryOptions };
1902
+ const provenance = provenanceOption !== false;
1903
+ if (Array.isArray(provenanceOption)) {
1904
+ coerceOptions.provenanceFields = provenanceOption;
1905
+ }
1546
1906
  if (!Number.isInteger(concurrency) || concurrency < 1) {
1547
1907
  throw new RangeError(`concurrency must be a positive integer, got ${String(concurrency)}`);
1548
1908
  }
1549
1909
  if (!Number.isInteger(retry.attempts) || retry.attempts < 0) {
1550
1910
  throw new RangeError(`retry.attempts must be a non-negative integer, got ${String(retry.attempts)}`);
1551
1911
  }
1552
- const results = new Array(inputs.length);
1912
+ const results = [];
1553
1913
  const gate = new BackoffGate(retry);
1554
- async function runOne(index) {
1914
+ const queue = new InputQueue(inputs);
1915
+ async function runOne(index, input) {
1555
1916
  let attempts = 0;
1556
1917
  let result;
1918
+ let usage = emptyUsage();
1919
+ const label = labelOf(input);
1920
+ const traceAttributes = { itemIndex: index, ...label !== void 0 ? { itemLabel: label } : {} };
1557
1921
  for (; ; ) {
1558
1922
  if (signal?.aborted) {
1559
- result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), attempts };
1923
+ result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), usage, attempts };
1560
1924
  break;
1561
1925
  }
1562
1926
  await gate.wait();
1563
1927
  attempts += 1;
1564
1928
  try {
1565
- const run = await runCoercion(inputs[index], coerceOptions, { mode, provenance });
1929
+ const run = await runCoercion(input, coerceOptions, { mode, provenance, traceAttributes });
1566
1930
  const data = mode === "partialCoerce" ? stripNulls(run.data) : run.data;
1567
1931
  gate.succeeded();
1568
- result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, attempts };
1932
+ result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, usage: run.usage, attempts };
1569
1933
  break;
1570
1934
  } catch (error) {
1935
+ usage = emptyUsage();
1571
1936
  if (isRetryable(error) && attempts <= retry.attempts) {
1572
1937
  gate.failed();
1573
1938
  continue;
1574
1939
  }
1575
- result = { ok: false, index, error, attempts };
1940
+ result = { ok: false, index, error, usage, attempts };
1576
1941
  break;
1577
1942
  }
1578
1943
  }
1579
1944
  results[index] = result;
1580
1945
  onItem?.(result);
1581
1946
  }
1582
- let next = 0;
1583
- if (primeCache && inputs.length > 1) {
1584
- await runOne(next++);
1947
+ let pending;
1948
+ const warmup = primed ?? (primeCache2 === "eager" ? primeCache({ ...coerceOptions, mode, provenance }) : void 0);
1949
+ if (warmup) {
1950
+ await Promise.resolve(warmup).catch(() => void 0);
1951
+ } else if (primeCache2 === true) {
1952
+ const first = await queue.next();
1953
+ if (first === void 0) return results;
1954
+ const second = await queue.next();
1955
+ if (second === void 0) {
1956
+ await runOne(first.index, first.input);
1957
+ return results;
1958
+ }
1959
+ await runOne(first.index, first.input);
1960
+ pending = second;
1585
1961
  }
1586
- const workers = Array.from({ length: Math.min(concurrency, inputs.length) }, async () => {
1587
- while (next < inputs.length) {
1588
- await runOne(next++);
1962
+ const workers = Array.from({ length: concurrency }, async () => {
1963
+ for (; ; ) {
1964
+ const item = pending ?? await queue.next();
1965
+ pending = void 0;
1966
+ if (item === void 0) return;
1967
+ await runOne(item.index, item.input);
1589
1968
  }
1590
1969
  });
1591
1970
  await Promise.all(workers);
@@ -1623,6 +2002,8 @@ function resolveConfig(callConfig) {
1623
2002
  traceSinks: callConfig?.traceSinks ?? global.traceSinks,
1624
2003
  maxRepairAttempts: callConfig?.maxRepairAttempts ?? global.maxRepairAttempts,
1625
2004
  onInvalidField: callConfig?.onInvalidField ?? global.onInvalidField,
2005
+ instructions: callConfig?.instructions ?? global.instructions,
2006
+ retryOnEmpty: callConfig?.retryOnEmpty ?? global.retryOnEmpty,
1626
2007
  maxInputChars: callConfig?.maxInputChars ?? global.maxInputChars,
1627
2008
  truncate: callConfig?.truncate ?? global.truncate,
1628
2009
  preprocess: callConfig?.preprocess ?? global.preprocess
@@ -1656,6 +2037,8 @@ var Coercible = class _Coercible {
1656
2037
  traceSinks: this._config.traceSinks,
1657
2038
  maxRepairAttempts: this._config.maxRepairAttempts,
1658
2039
  onInvalidField: this._config.onInvalidField,
2040
+ instructions: this._config.instructions,
2041
+ retryOnEmpty: this._config.retryOnEmpty,
1659
2042
  maxInputChars: this._config.maxInputChars,
1660
2043
  truncate: this._config.truncate,
1661
2044
  preprocess: this._config.preprocess
@@ -1722,6 +2105,7 @@ var ConsoleSink = class {
1722
2105
  Constrain,
1723
2106
  Describe,
1724
2107
  EnumResolutionError,
2108
+ FIELD_FORMATS,
1725
2109
  PROVENANCE_INSTRUCTIONS,
1726
2110
  SOURCE_INSTRUCTIONS,
1727
2111
  Schema,
@@ -1734,15 +2118,21 @@ var ConsoleSink = class {
1734
2118
  buildRepairInput,
1735
2119
  bundleOf,
1736
2120
  coerce,
2121
+ coerceDetailed,
1737
2122
  coerceMany,
1738
2123
  coerceWithProvenance,
1739
2124
  collectEnumSources,
1740
2125
  defineSchema,
2126
+ describeFormat,
1741
2127
  field,
2128
+ formatToJsonSchema,
1742
2129
  isCoerceInput,
1743
2130
  isSource,
2131
+ normalizeInstructions,
1744
2132
  partialCoerce,
2133
+ partialCoerceDetailed,
1745
2134
  partialCoerceWithProvenance,
2135
+ primeCache,
1746
2136
  provenanceInstructions,
1747
2137
  renderSources,
1748
2138
  resolveEnumSources,
@@ -1753,6 +2143,7 @@ var ConsoleSink = class {
1753
2143
  toOpenAIJsonSchema,
1754
2144
  toProvenanceSchema,
1755
2145
  toSources,
2146
+ validateFormat,
1756
2147
  validatePartial,
1757
2148
  validateStrict
1758
2149
  });