@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.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,16 +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,
48
52
  normalizeInstructions: () => normalizeInstructions,
49
53
  partialCoerce: () => partialCoerce,
54
+ partialCoerceDetailed: () => partialCoerceDetailed,
50
55
  partialCoerceWithProvenance: () => partialCoerceWithProvenance,
56
+ primeCache: () => primeCache,
51
57
  provenanceInstructions: () => provenanceInstructions,
52
58
  renderSources: () => renderSources,
53
59
  resolveEnumSources: () => resolveEnumSources,
@@ -58,11 +64,164 @@ __export(index_exports, {
58
64
  toOpenAIJsonSchema: () => toOpenAIJsonSchema,
59
65
  toProvenanceSchema: () => toProvenanceSchema,
60
66
  toSources: () => toSources,
67
+ validateFormat: () => validateFormat,
61
68
  validatePartial: () => validatePartial,
62
69
  validateStrict: () => validateStrict
63
70
  });
64
71
  module.exports = __toCommonJS(index_exports);
65
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
+
66
225
  // src/schema/json-schema.ts
67
226
  var CONSTRAINT_KEYWORDS = [
68
227
  "maxLength",
@@ -79,6 +238,9 @@ function constraintsToJsonSchema(constraints, dialect) {
79
238
  return {};
80
239
  }
81
240
  const out = {};
241
+ if (constraints.format !== void 0) {
242
+ Object.assign(out, formatToJsonSchema(constraints.format));
243
+ }
82
244
  for (const keyword of CONSTRAINT_KEYWORDS) {
83
245
  const value = constraints[keyword];
84
246
  if (value !== void 0) {
@@ -463,7 +625,7 @@ ${summary}`);
463
625
  // src/coerce/sources.ts
464
626
  var SOURCE_TAG = "source";
465
627
  function isSource(value) {
466
- 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");
467
629
  }
468
630
  function isCoerceInput(value) {
469
631
  return typeof value === "string" || isSource(value) || Array.isArray(value) && value.every(isSource);
@@ -483,7 +645,9 @@ function toSources(input) {
483
645
  }
484
646
  function cleanLabel(source) {
485
647
  const label = source.label?.trim();
486
- 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;
487
651
  }
488
652
  function escapeText(text) {
489
653
  return text.replace(new RegExp(`</(\\s*${SOURCE_TAG}\\b)`, "gi"), "<\\/$1");
@@ -521,7 +685,10 @@ function describeConstraints(constraints) {
521
685
  return [];
522
686
  }
523
687
  const phrases = [];
524
- 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
+ }
525
692
  if (minLength !== void 0 && maxLength !== void 0) {
526
693
  phrases.push(`between ${minLength} and ${maxLength} characters`);
527
694
  } else if (maxLength !== void 0) {
@@ -634,6 +801,8 @@ function buildPrompt(schema, bundle, options = {}) {
634
801
  lines.push("Instructions:");
635
802
  lines.push("- Extract values from the sources that match the schema fields.");
636
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.");
637
806
  lines.push("- Required fields must always have a valid, non-null value.");
638
807
  lines.push("- Interpret the user's input semantically \u2014 infer meaning, don't just pattern match.");
639
808
  lines.push("- Respect every stated limit exactly; truncate or drop lower-priority content to stay within it.");
@@ -699,11 +868,31 @@ var PROVENANCE_INSTRUCTIONS = [
699
868
  "- Judge each field on its own. A confident value next to a guessed one is",
700
869
  " normal, and marking the guess honestly is more useful than looking sure."
701
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
+ }
702
883
  function provenanceInstructions(options = {}) {
703
884
  const labels = options.sourceLabels ?? [];
704
- if (labels.length < 2) return PROVENANCE_INSTRUCTIONS;
705
- return `${PROVENANCE_INSTRUCTIONS}
706
- - 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;
707
896
  }
708
897
  function annotationSchema(parentId, field2, sourceLabels) {
709
898
  const valueField = {
@@ -735,7 +924,7 @@ function annotationSchema(parentId, field2, sourceLabels) {
735
924
  name: "source",
736
925
  description: "The label of the source this value was read from.",
737
926
  type: { kind: "enum", values: [...sourceLabels] },
738
- required: false
927
+ required: true
739
928
  }
740
929
  ] : []
741
930
  ]
@@ -745,7 +934,12 @@ function toProvenanceSchema(schema, bundle, options = {}) {
745
934
  const schemas = { ...bundle?.schemas ?? {} };
746
935
  const fields = [];
747
936
  const sourceLabels = options.sourceLabels ?? [];
937
+ const wrapped = provenanceFieldNames(schema, options.fields);
748
938
  for (const field2 of schema.fields) {
939
+ if (!wrapped.has(field2.name)) {
940
+ fields.push(field2);
941
+ continue;
942
+ }
749
943
  const annotation = annotationSchema(schema.id, field2, sourceLabels);
750
944
  schemas[annotation.id] = annotation;
751
945
  fields.push({
@@ -821,10 +1015,30 @@ ${tailRoom > 0 ? text.slice(text.length - tailRoom) : ""}`;
821
1015
  }
822
1016
  }
823
1017
  function budgetSources(sources, maxChars, policy = "tail") {
824
- const total = sources.reduce((sum, s) => sum + s.text.length, 0);
825
- if (total <= maxChars) {
826
- 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()] };
827
1040
  }
1041
+ sources = capped;
828
1042
  const allowance = /* @__PURE__ */ new Map();
829
1043
  const order = sources.map((s, i) => i).sort((a, b) => sources[a].text.length - sources[b].text.length);
830
1044
  let remaining = maxChars;
@@ -834,19 +1048,14 @@ function budgetSources(sources, maxChars, policy = "tail") {
834
1048
  allowance.set(index, granted);
835
1049
  remaining -= granted;
836
1050
  });
837
- const truncated = [];
838
1051
  const budgeted = sources.map((source, index) => {
839
1052
  const limit = allowance.get(index) ?? 0;
840
1053
  if (source.text.length <= limit) return source;
841
1054
  const text = truncateText(source.text, limit, policy);
842
- truncated.push({
843
- ...source.label !== void 0 ? { label: source.label } : {},
844
- originalLength: source.text.length,
845
- keptLength: text.length
846
- });
1055
+ record(index, source, text);
847
1056
  return { ...source, text };
848
1057
  });
849
- return { sources: budgeted, truncated };
1058
+ return { sources: budgeted, truncated: [...records.values()] };
850
1059
  }
851
1060
 
852
1061
  // src/coerce/validator.ts
@@ -862,7 +1071,7 @@ function entries(count) {
862
1071
  return `${count} ${count === 1 ? "entry" : "entries"}`;
863
1072
  }
864
1073
  function validateConstraints(value, constraints, path, issues) {
865
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
1074
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
866
1075
  if (Array.isArray(value)) {
867
1076
  if (minItems !== void 0 && value.length < minItems) {
868
1077
  issues.push({
@@ -906,6 +1115,10 @@ function validateConstraints(value, constraints, path, issues) {
906
1115
  received: value
907
1116
  });
908
1117
  }
1118
+ if (format !== void 0) {
1119
+ const message = validateFormat(value, format);
1120
+ if (message) issues.push({ path, message, received: value });
1121
+ }
909
1122
  return;
910
1123
  }
911
1124
  if (typeof value === "number") {
@@ -1247,8 +1460,15 @@ function generateSpanId() {
1247
1460
  }
1248
1461
  var Tracer = class {
1249
1462
  sinks;
1250
- 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) {
1251
1470
  this.sinks = sinks ?? [];
1471
+ this.baseAttributes = baseAttributes;
1252
1472
  }
1253
1473
  startSpan(name, attributes, parent) {
1254
1474
  return {
@@ -1256,7 +1476,7 @@ var Tracer = class {
1256
1476
  name,
1257
1477
  startTime: Date.now(),
1258
1478
  events: [],
1259
- attributes,
1479
+ attributes: this.baseAttributes ? { ...this.baseAttributes, ...attributes } : attributes,
1260
1480
  parentId: parent?.id
1261
1481
  };
1262
1482
  }
@@ -1311,15 +1531,31 @@ async function resolveEnums(schema, bundle, enumResolver, tracer, parent) {
1311
1531
  tracer.endSpan(span);
1312
1532
  }
1313
1533
  }
1314
- async function runCoercion(input, options, { mode, provenance }) {
1315
- const { provider, schema, enumResolver, traceSinks } = options;
1316
- 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) {
1317
1547
  const maxRepairAttempts = options.maxRepairAttempts ?? 0;
1318
1548
  if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
1319
1549
  throw new RangeError(
1320
1550
  `maxRepairAttempts must be a non-negative integer, got ${String(options.maxRepairAttempts)}`
1321
1551
  );
1322
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
+ }
1323
1559
  const instructions = normalizeInstructions(options.instructions);
1324
1560
  const onInvalidField = options.onInvalidField ?? "throw";
1325
1561
  if (!INVALID_FIELD_POLICIES.includes(onInvalidField)) {
@@ -1332,61 +1568,96 @@ async function runCoercion(input, options, { mode, provenance }) {
1332
1568
  `maxInputChars must be a positive integer, got ${String(options.maxInputChars)}`
1333
1569
  );
1334
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);
1335
1604
  const rawSources = toSources(input);
1336
- const tracer = new Tracer(traceSinks);
1605
+ const tracer = new Tracer(traceSinks, traceAttributes);
1337
1606
  const rootSpan = tracer.startSpan(mode, {
1338
1607
  schemaId: schema.id,
1339
1608
  provenance,
1340
1609
  onInvalidField,
1341
1610
  sourceCount: rawSources.length
1342
1611
  });
1612
+ const usage = emptyUsage();
1343
1613
  try {
1344
1614
  const sources = await prepareSources(rawSources, options, tracer, rootSpan);
1345
1615
  const sourceLabels = sources.length > 1 ? sources.map((s) => s.label ?? "") : [];
1346
- const resolvedEnums = await resolveEnums(
1347
- schema,
1348
- bundle,
1349
- enumResolver,
1616
+ const prepared = await prepareRequest(
1617
+ options,
1618
+ { mode, provenance },
1619
+ instructions,
1620
+ sourceLabels,
1350
1621
  tracer,
1351
1622
  rootSpan
1352
1623
  );
1353
- const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1354
- const basePrompt = buildPrompt(schema, bundle, { resolvedEnums, instructions });
1355
- const systemPrompt = provenance ? `${basePrompt}
1356
- ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1357
- tracer.addEvent(promptSpan, "promptBuilt", {
1358
- promptLength: systemPrompt.length,
1359
- instructionCount: instructions.length
1360
- });
1361
- tracer.endSpan(promptSpan);
1362
- const request = provenance ? toProvenanceSchema(schema, bundle, { sourceLabels }) : { schema, bundle };
1363
- const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1364
- const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1365
- resolvedEnums
1366
- });
1367
- tracer.endSpan(schemaSpan);
1624
+ const { systemPrompt, jsonSchema, resolvedEnums } = prepared;
1368
1625
  const validate = mode === "coerce" ? validateStrict : validatePartial;
1369
1626
  const renderedInput = renderSources(sources);
1627
+ const hasInput = sources.some((s) => s.text.trim().length > 0);
1370
1628
  tracer.addEvent(rootSpan, "inputRendered", {
1371
1629
  sourceCount: sources.length,
1372
1630
  inputLength: renderedInput.length
1373
1631
  });
1374
1632
  let userInput = renderedInput;
1375
1633
  let issues = [];
1376
- let run = { data: {}, provenance: {}, issues: [] };
1634
+ let run = { data: {}, provenance: {}, issues: [], usage };
1635
+ let emptyRetries = 0;
1377
1636
  for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
1378
1637
  const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
1379
1638
  const response = await provider.complete({
1380
1639
  systemPrompt,
1381
1640
  userInput,
1382
1641
  jsonSchema,
1383
- schema: request.schema,
1384
- bundle: request.bundle,
1642
+ schema: prepared.schema,
1643
+ bundle: prepared.bundle,
1385
1644
  resolvedEnums
1386
1645
  });
1646
+ addUsage(usage, response.usage);
1387
1647
  tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1388
1648
  tracer.endSpan(llmSpan);
1389
- 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
+ }
1390
1661
  const validationSpan = tracer.startSpan("validate", { attempt }, rootSpan);
1391
1662
  issues = validate(run.data, schema, bundle, { resolvedEnums });
1392
1663
  tracer.addEvent(validationSpan, "validated", { issueCount: issues.length });
@@ -1412,7 +1683,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1412
1683
  return {
1413
1684
  data: outcome.data,
1414
1685
  provenance: pruneProvenance(run.provenance, outcome.resolved),
1415
- issues: outcome.resolved
1686
+ issues: outcome.resolved,
1687
+ usage
1416
1688
  };
1417
1689
  }
1418
1690
  }
@@ -1433,7 +1705,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1433
1705
  }
1434
1706
  async function prepareSources(sources, options, tracer, parent) {
1435
1707
  const { preprocess, maxInputChars, truncate } = options;
1436
- if (!preprocess && maxInputChars === void 0) {
1708
+ const anyCapped = sources.some((s) => s.maxChars !== void 0);
1709
+ if (!preprocess && maxInputChars === void 0 && !anyCapped) {
1437
1710
  return [...sources];
1438
1711
  }
1439
1712
  const span = tracer.startSpan("prepareInput", {}, parent);
@@ -1450,7 +1723,7 @@ async function prepareSources(sources, options, tracer, parent) {
1450
1723
  lengths: prepared.map((s) => s.text.length)
1451
1724
  });
1452
1725
  }
1453
- if (maxInputChars !== void 0) {
1726
+ if (maxInputChars !== void 0 || prepared.some((s) => s.maxChars !== void 0)) {
1454
1727
  const budgeted = budgetSources(prepared, maxInputChars, truncate);
1455
1728
  prepared = budgeted.sources;
1456
1729
  if (budgeted.truncated.length > 0) {
@@ -1498,19 +1771,59 @@ async function partialCoerce(input, options) {
1498
1771
  });
1499
1772
  return stripNulls(data);
1500
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
+ }
1501
1814
  async function coerceWithProvenance(input, options) {
1502
- const { data, provenance, issues } = await runCoercion(input, options, {
1815
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1503
1816
  mode: "coerce",
1504
1817
  provenance: true
1505
1818
  });
1506
- return { data, provenance, issues };
1819
+ return { data, provenance, issues, usage };
1507
1820
  }
1508
1821
  async function partialCoerceWithProvenance(input, options) {
1509
- const { data, provenance, issues } = await runCoercion(input, options, {
1822
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1510
1823
  mode: "partialCoerce",
1511
1824
  provenance: true
1512
1825
  });
1513
- return { data: stripNulls(data), provenance, issues };
1826
+ return { data: stripNulls(data), provenance, issues, usage };
1514
1827
  }
1515
1828
 
1516
1829
  // src/coerce/coerce-many.ts
@@ -1551,60 +1864,107 @@ var BackoffGate = class {
1551
1864
  this.streak = 0;
1552
1865
  }
1553
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
+ }
1554
1889
  async function coerceMany(inputs, options) {
1555
1890
  const {
1556
1891
  concurrency = DEFAULT_CONCURRENCY,
1557
1892
  mode = "coerce",
1558
- provenance = false,
1559
- primeCache = true,
1893
+ provenance: provenanceOption = false,
1894
+ primeCache: primeCache2 = true,
1895
+ primed,
1560
1896
  onItem,
1561
1897
  signal,
1898
+ retry: retryOptions,
1562
1899
  ...coerceOptions
1563
1900
  } = options;
1564
- 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
+ }
1565
1906
  if (!Number.isInteger(concurrency) || concurrency < 1) {
1566
1907
  throw new RangeError(`concurrency must be a positive integer, got ${String(concurrency)}`);
1567
1908
  }
1568
1909
  if (!Number.isInteger(retry.attempts) || retry.attempts < 0) {
1569
1910
  throw new RangeError(`retry.attempts must be a non-negative integer, got ${String(retry.attempts)}`);
1570
1911
  }
1571
- const results = new Array(inputs.length);
1912
+ const results = [];
1572
1913
  const gate = new BackoffGate(retry);
1573
- async function runOne(index) {
1914
+ const queue = new InputQueue(inputs);
1915
+ async function runOne(index, input) {
1574
1916
  let attempts = 0;
1575
1917
  let result;
1918
+ let usage = emptyUsage();
1919
+ const label = labelOf(input);
1920
+ const traceAttributes = { itemIndex: index, ...label !== void 0 ? { itemLabel: label } : {} };
1576
1921
  for (; ; ) {
1577
1922
  if (signal?.aborted) {
1578
- 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 };
1579
1924
  break;
1580
1925
  }
1581
1926
  await gate.wait();
1582
1927
  attempts += 1;
1583
1928
  try {
1584
- const run = await runCoercion(inputs[index], coerceOptions, { mode, provenance });
1929
+ const run = await runCoercion(input, coerceOptions, { mode, provenance, traceAttributes });
1585
1930
  const data = mode === "partialCoerce" ? stripNulls(run.data) : run.data;
1586
1931
  gate.succeeded();
1587
- 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 };
1588
1933
  break;
1589
1934
  } catch (error) {
1935
+ usage = emptyUsage();
1590
1936
  if (isRetryable(error) && attempts <= retry.attempts) {
1591
1937
  gate.failed();
1592
1938
  continue;
1593
1939
  }
1594
- result = { ok: false, index, error, attempts };
1940
+ result = { ok: false, index, error, usage, attempts };
1595
1941
  break;
1596
1942
  }
1597
1943
  }
1598
1944
  results[index] = result;
1599
1945
  onItem?.(result);
1600
1946
  }
1601
- let next = 0;
1602
- if (primeCache && inputs.length > 1) {
1603
- 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;
1604
1961
  }
1605
- const workers = Array.from({ length: Math.min(concurrency, inputs.length) }, async () => {
1606
- while (next < inputs.length) {
1607
- 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);
1608
1968
  }
1609
1969
  });
1610
1970
  await Promise.all(workers);
@@ -1643,6 +2003,7 @@ function resolveConfig(callConfig) {
1643
2003
  maxRepairAttempts: callConfig?.maxRepairAttempts ?? global.maxRepairAttempts,
1644
2004
  onInvalidField: callConfig?.onInvalidField ?? global.onInvalidField,
1645
2005
  instructions: callConfig?.instructions ?? global.instructions,
2006
+ retryOnEmpty: callConfig?.retryOnEmpty ?? global.retryOnEmpty,
1646
2007
  maxInputChars: callConfig?.maxInputChars ?? global.maxInputChars,
1647
2008
  truncate: callConfig?.truncate ?? global.truncate,
1648
2009
  preprocess: callConfig?.preprocess ?? global.preprocess
@@ -1677,6 +2038,7 @@ var Coercible = class _Coercible {
1677
2038
  maxRepairAttempts: this._config.maxRepairAttempts,
1678
2039
  onInvalidField: this._config.onInvalidField,
1679
2040
  instructions: this._config.instructions,
2041
+ retryOnEmpty: this._config.retryOnEmpty,
1680
2042
  maxInputChars: this._config.maxInputChars,
1681
2043
  truncate: this._config.truncate,
1682
2044
  preprocess: this._config.preprocess
@@ -1743,6 +2105,7 @@ var ConsoleSink = class {
1743
2105
  Constrain,
1744
2106
  Describe,
1745
2107
  EnumResolutionError,
2108
+ FIELD_FORMATS,
1746
2109
  PROVENANCE_INSTRUCTIONS,
1747
2110
  SOURCE_INSTRUCTIONS,
1748
2111
  Schema,
@@ -1755,16 +2118,21 @@ var ConsoleSink = class {
1755
2118
  buildRepairInput,
1756
2119
  bundleOf,
1757
2120
  coerce,
2121
+ coerceDetailed,
1758
2122
  coerceMany,
1759
2123
  coerceWithProvenance,
1760
2124
  collectEnumSources,
1761
2125
  defineSchema,
2126
+ describeFormat,
1762
2127
  field,
2128
+ formatToJsonSchema,
1763
2129
  isCoerceInput,
1764
2130
  isSource,
1765
2131
  normalizeInstructions,
1766
2132
  partialCoerce,
2133
+ partialCoerceDetailed,
1767
2134
  partialCoerceWithProvenance,
2135
+ primeCache,
1768
2136
  provenanceInstructions,
1769
2137
  renderSources,
1770
2138
  resolveEnumSources,
@@ -1775,6 +2143,7 @@ var ConsoleSink = class {
1775
2143
  toOpenAIJsonSchema,
1776
2144
  toProvenanceSchema,
1777
2145
  toSources,
2146
+ validateFormat,
1778
2147
  validatePartial,
1779
2148
  validateStrict
1780
2149
  });