@sembl/core 0.3.0 → 0.5.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,
@@ -35,19 +36,25 @@ __export(index_exports, {
35
36
  ValuesFrom: () => ValuesFrom,
36
37
  budgetSources: () => budgetSources,
37
38
  buildPrompt: () => buildPrompt,
39
+ buildRepairCorrection: () => buildRepairCorrection,
38
40
  buildRepairInput: () => buildRepairInput,
39
41
  bundleOf: () => bundleOf,
40
42
  coerce: () => coerce,
43
+ coerceDetailed: () => coerceDetailed,
41
44
  coerceMany: () => coerceMany,
42
45
  coerceWithProvenance: () => coerceWithProvenance,
43
46
  collectEnumSources: () => collectEnumSources,
44
47
  defineSchema: () => defineSchema,
48
+ describeFormat: () => describeFormat,
45
49
  field: () => field,
50
+ formatToJsonSchema: () => formatToJsonSchema,
46
51
  isCoerceInput: () => isCoerceInput,
47
52
  isSource: () => isSource,
48
53
  normalizeInstructions: () => normalizeInstructions,
49
54
  partialCoerce: () => partialCoerce,
55
+ partialCoerceDetailed: () => partialCoerceDetailed,
50
56
  partialCoerceWithProvenance: () => partialCoerceWithProvenance,
57
+ primeCache: () => primeCache,
51
58
  provenanceInstructions: () => provenanceInstructions,
52
59
  renderSources: () => renderSources,
53
60
  resolveEnumSources: () => resolveEnumSources,
@@ -58,11 +65,164 @@ __export(index_exports, {
58
65
  toOpenAIJsonSchema: () => toOpenAIJsonSchema,
59
66
  toProvenanceSchema: () => toProvenanceSchema,
60
67
  toSources: () => toSources,
68
+ validateFormat: () => validateFormat,
61
69
  validatePartial: () => validatePartial,
62
70
  validateStrict: () => validateStrict
63
71
  });
64
72
  module.exports = __toCommonJS(index_exports);
65
73
 
74
+ // src/schema/formats.ts
75
+ var FIELD_FORMATS = [
76
+ "url",
77
+ "email",
78
+ "date",
79
+ "datetime",
80
+ "iso-country",
81
+ "us-state",
82
+ "us-state-name",
83
+ "currency"
84
+ ];
85
+ var ISO_COUNTRIES = new Set(
86
+ "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(" ")
87
+ );
88
+ var US_STATES = {
89
+ AL: "Alabama",
90
+ AK: "Alaska",
91
+ AZ: "Arizona",
92
+ AR: "Arkansas",
93
+ CA: "California",
94
+ CO: "Colorado",
95
+ CT: "Connecticut",
96
+ DE: "Delaware",
97
+ FL: "Florida",
98
+ GA: "Georgia",
99
+ HI: "Hawaii",
100
+ ID: "Idaho",
101
+ IL: "Illinois",
102
+ IN: "Indiana",
103
+ IA: "Iowa",
104
+ KS: "Kansas",
105
+ KY: "Kentucky",
106
+ LA: "Louisiana",
107
+ ME: "Maine",
108
+ MD: "Maryland",
109
+ MA: "Massachusetts",
110
+ MI: "Michigan",
111
+ MN: "Minnesota",
112
+ MS: "Mississippi",
113
+ MO: "Missouri",
114
+ MT: "Montana",
115
+ NE: "Nebraska",
116
+ NV: "Nevada",
117
+ NH: "New Hampshire",
118
+ NJ: "New Jersey",
119
+ NM: "New Mexico",
120
+ NY: "New York",
121
+ NC: "North Carolina",
122
+ ND: "North Dakota",
123
+ OH: "Ohio",
124
+ OK: "Oklahoma",
125
+ OR: "Oregon",
126
+ PA: "Pennsylvania",
127
+ RI: "Rhode Island",
128
+ SC: "South Carolina",
129
+ SD: "South Dakota",
130
+ TN: "Tennessee",
131
+ TX: "Texas",
132
+ UT: "Utah",
133
+ VT: "Vermont",
134
+ VA: "Virginia",
135
+ WA: "Washington",
136
+ WV: "West Virginia",
137
+ WI: "Wisconsin",
138
+ WY: "Wyoming",
139
+ DC: "District of Columbia",
140
+ PR: "Puerto Rico",
141
+ GU: "Guam",
142
+ VI: "U.S. Virgin Islands",
143
+ AS: "American Samoa",
144
+ MP: "Northern Mariana Islands"
145
+ };
146
+ var US_STATE_NAMES = new Set(Object.values(US_STATES));
147
+ var CURRENCIES = new Set(
148
+ "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(" ")
149
+ );
150
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
151
+ var DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
152
+ var DATETIME = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/;
153
+ function isCalendarDate(y, m, d) {
154
+ const date = new Date(Date.UTC(y, m - 1, d));
155
+ return date.getUTCFullYear() === y && date.getUTCMonth() === m - 1 && date.getUTCDate() === d;
156
+ }
157
+ function validateFormat(value, format) {
158
+ switch (format) {
159
+ case "url": {
160
+ try {
161
+ const url = new URL(value);
162
+ if (url.protocol === "http:" || url.protocol === "https:") return void 0;
163
+ } catch {
164
+ }
165
+ return `Expected an absolute http(s) URL, got ${JSON.stringify(value)}`;
166
+ }
167
+ case "email":
168
+ return EMAIL.test(value) ? void 0 : `Expected an email address, got ${JSON.stringify(value)}`;
169
+ case "date": {
170
+ const m = DATE.exec(value);
171
+ if (m && isCalendarDate(Number(m[1]), Number(m[2]), Number(m[3]))) return void 0;
172
+ return `Expected a calendar date as YYYY-MM-DD, got ${JSON.stringify(value)}`;
173
+ }
174
+ case "datetime":
175
+ return DATETIME.test(value) && !Number.isNaN(Date.parse(value)) ? void 0 : `Expected an ISO 8601 timestamp, got ${JSON.stringify(value)}`;
176
+ case "iso-country":
177
+ 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)}`;
178
+ case "us-state":
179
+ return value in US_STATES ? void 0 : `Expected a two-letter USPS state code such as CA or NY, got ${JSON.stringify(value)}`;
180
+ case "us-state-name":
181
+ return US_STATE_NAMES.has(value) ? void 0 : `Expected a US state's full name such as California, got ${JSON.stringify(value)}`;
182
+ case "currency":
183
+ return CURRENCIES.has(value) ? void 0 : `Expected an ISO 4217 currency code such as USD or EUR, got ${JSON.stringify(value)}`;
184
+ }
185
+ }
186
+ function describeFormat(format) {
187
+ switch (format) {
188
+ case "url":
189
+ return "an absolute http(s) URL";
190
+ case "email":
191
+ return "an email address";
192
+ case "date":
193
+ return "a calendar date as YYYY-MM-DD";
194
+ case "datetime":
195
+ return "an ISO 8601 timestamp (e.g. 2026-09-05T14:30:00Z)";
196
+ case "iso-country":
197
+ return "an ISO 3166-1 alpha-2 country code (e.g. US, DE, PT), never a country name";
198
+ case "us-state":
199
+ return "a two-letter USPS state code (e.g. CA, NY), never the state's name";
200
+ case "us-state-name":
201
+ return "a US state's full name (e.g. California, New York), never its abbreviation";
202
+ case "currency":
203
+ return "an ISO 4217 currency code (e.g. USD, EUR, GBP), never a symbol or a word";
204
+ }
205
+ }
206
+ function formatToJsonSchema(format) {
207
+ switch (format) {
208
+ case "url":
209
+ return { format: "uri" };
210
+ case "email":
211
+ return { format: "email" };
212
+ case "date":
213
+ return { format: "date" };
214
+ case "datetime":
215
+ return { format: "date-time" };
216
+ case "iso-country":
217
+ case "us-state":
218
+ return { pattern: "^[A-Z]{2}$" };
219
+ case "currency":
220
+ return { pattern: "^[A-Z]{3}$" };
221
+ case "us-state-name":
222
+ return {};
223
+ }
224
+ }
225
+
66
226
  // src/schema/json-schema.ts
67
227
  var CONSTRAINT_KEYWORDS = [
68
228
  "maxLength",
@@ -79,6 +239,9 @@ function constraintsToJsonSchema(constraints, dialect) {
79
239
  return {};
80
240
  }
81
241
  const out = {};
242
+ if (constraints.format !== void 0) {
243
+ Object.assign(out, formatToJsonSchema(constraints.format));
244
+ }
82
245
  for (const keyword of CONSTRAINT_KEYWORDS) {
83
246
  const value = constraints[keyword];
84
247
  if (value !== void 0) {
@@ -228,7 +391,7 @@ async function resolveEnumSources(schema, resolver, bundle) {
228
391
  await Promise.all(
229
392
  [...usages].map(async ([sourceId, usage]) => {
230
393
  try {
231
- const values = await resolver(sourceId);
394
+ const values = await resolver(sourceId, { sourceId, schema, ...usage });
232
395
  if (!values || values.length === 0) {
233
396
  failures.push({ sourceId, reason: "empty", ...usage });
234
397
  return;
@@ -463,7 +626,7 @@ ${summary}`);
463
626
  // src/coerce/sources.ts
464
627
  var SOURCE_TAG = "source";
465
628
  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");
629
+ 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
630
  }
468
631
  function isCoerceInput(value) {
469
632
  return typeof value === "string" || isSource(value) || Array.isArray(value) && value.every(isSource);
@@ -483,7 +646,9 @@ function toSources(input) {
483
646
  }
484
647
  function cleanLabel(source) {
485
648
  const label = source.label?.trim();
486
- return label ? { label, text: source.text } : { text: source.text };
649
+ const cleaned = label ? { label, text: source.text } : { text: source.text };
650
+ if (source.maxChars !== void 0) cleaned.maxChars = source.maxChars;
651
+ return cleaned;
487
652
  }
488
653
  function escapeText(text) {
489
654
  return text.replace(new RegExp(`</(\\s*${SOURCE_TAG}\\b)`, "gi"), "<\\/$1");
@@ -521,7 +686,10 @@ function describeConstraints(constraints) {
521
686
  return [];
522
687
  }
523
688
  const phrases = [];
524
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
689
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
690
+ if (format !== void 0) {
691
+ phrases.push(describeFormat(format));
692
+ }
525
693
  if (minLength !== void 0 && maxLength !== void 0) {
526
694
  phrases.push(`between ${minLength} and ${maxLength} characters`);
527
695
  } else if (maxLength !== void 0) {
@@ -634,6 +802,8 @@ function buildPrompt(schema, bundle, options = {}) {
634
802
  lines.push("Instructions:");
635
803
  lines.push("- Extract values from the sources that match the schema fields.");
636
804
  lines.push("- Use null for optional fields that cannot be determined from the input.");
805
+ 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.");
806
+ lines.push("- Never return an empty object when the input states values for any field.");
637
807
  lines.push("- Required fields must always have a valid, non-null value.");
638
808
  lines.push("- Interpret the user's input semantically \u2014 infer meaning, don't just pattern match.");
639
809
  lines.push("- Respect every stated limit exactly; truncate or drop lower-priority content to stay within it.");
@@ -659,7 +829,7 @@ function renderReceived(received) {
659
829
  return text.length > MAX_RECEIVED_LENGTH ? `${text.slice(0, MAX_RECEIVED_LENGTH)}\u2026 (truncated)` : text;
660
830
  }
661
831
  function buildRepairInput(originalInput, rejected, issues) {
662
- const lines = [
832
+ return [
663
833
  originalInput,
664
834
  "",
665
835
  "---",
@@ -668,9 +838,11 @@ function buildRepairInput(originalInput, rejected, issues) {
668
838
  "",
669
839
  JSON.stringify(rejected, null, 2),
670
840
  "",
671
- "It was rejected because:",
672
- ""
673
- ];
841
+ buildRepairCorrection(issues)
842
+ ].join("\n");
843
+ }
844
+ function buildRepairCorrection(issues) {
845
+ const lines = ["The output was rejected because:", ""];
674
846
  for (const issue of issues) {
675
847
  lines.push(`- ${issue.path}: ${issue.message} (received: ${renderReceived(issue.received)})`);
676
848
  }
@@ -699,11 +871,31 @@ var PROVENANCE_INSTRUCTIONS = [
699
871
  "- Judge each field on its own. A confident value next to a guessed one is",
700
872
  " normal, and marking the guess honestly is more useful than looking sure."
701
873
  ].join("\n");
874
+ function provenanceFieldNames(schema, fields) {
875
+ if (fields === void 0) return new Set(schema.fields.map((f) => f.name));
876
+ const known = new Set(schema.fields.map((f) => f.name));
877
+ for (const name of fields) {
878
+ if (!known.has(name)) {
879
+ throw new RangeError(
880
+ `provenance field "${name}" is not a field of schema "${schema.id}" (fields: ${[...known].join(", ")})`
881
+ );
882
+ }
883
+ }
884
+ return new Set(fields);
885
+ }
702
886
  function provenanceInstructions(options = {}) {
703
887
  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.`;
888
+ let text = PROVENANCE_INSTRUCTIONS;
889
+ if (options.fields !== void 0) {
890
+ text = text.replace(
891
+ "- Every field is wrapped as an object: put the extracted value in `value`.",
892
+ `- Only these fields are wrapped as objects, with the extracted value in \`value\`: ${options.fields.join(", ")}. Every other field is a plain value.`
893
+ );
894
+ }
895
+ if (labels.length >= 2) {
896
+ 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`.";
897
+ }
898
+ return text;
707
899
  }
708
900
  function annotationSchema(parentId, field2, sourceLabels) {
709
901
  const valueField = {
@@ -735,7 +927,7 @@ function annotationSchema(parentId, field2, sourceLabels) {
735
927
  name: "source",
736
928
  description: "The label of the source this value was read from.",
737
929
  type: { kind: "enum", values: [...sourceLabels] },
738
- required: false
930
+ required: true
739
931
  }
740
932
  ] : []
741
933
  ]
@@ -745,7 +937,12 @@ function toProvenanceSchema(schema, bundle, options = {}) {
745
937
  const schemas = { ...bundle?.schemas ?? {} };
746
938
  const fields = [];
747
939
  const sourceLabels = options.sourceLabels ?? [];
940
+ const wrapped = provenanceFieldNames(schema, options.fields);
748
941
  for (const field2 of schema.fields) {
942
+ if (!wrapped.has(field2.name)) {
943
+ fields.push(field2);
944
+ continue;
945
+ }
749
946
  const annotation = annotationSchema(schema.id, field2, sourceLabels);
750
947
  schemas[annotation.id] = annotation;
751
948
  fields.push({
@@ -821,10 +1018,30 @@ ${tailRoom > 0 ? text.slice(text.length - tailRoom) : ""}`;
821
1018
  }
822
1019
  }
823
1020
  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: [] };
1021
+ const records = /* @__PURE__ */ new Map();
1022
+ const record = (index, source, text) => {
1023
+ const existing = records.get(index);
1024
+ if (existing) {
1025
+ existing.keptLength = text.length;
1026
+ } else {
1027
+ records.set(index, {
1028
+ ...source.label !== void 0 ? { label: source.label } : {},
1029
+ originalLength: source.text.length,
1030
+ keptLength: text.length
1031
+ });
1032
+ }
1033
+ };
1034
+ const capped = sources.map((source, index) => {
1035
+ if (source.maxChars === void 0 || source.text.length <= source.maxChars) return source;
1036
+ const text = truncateText(source.text, source.maxChars, policy);
1037
+ record(index, source, text);
1038
+ return { ...source, text };
1039
+ });
1040
+ const total = capped.reduce((sum, s) => sum + s.text.length, 0);
1041
+ if (maxChars === void 0 || total <= maxChars) {
1042
+ return { sources: capped, truncated: [...records.values()] };
827
1043
  }
1044
+ sources = capped;
828
1045
  const allowance = /* @__PURE__ */ new Map();
829
1046
  const order = sources.map((s, i) => i).sort((a, b) => sources[a].text.length - sources[b].text.length);
830
1047
  let remaining = maxChars;
@@ -834,19 +1051,14 @@ function budgetSources(sources, maxChars, policy = "tail") {
834
1051
  allowance.set(index, granted);
835
1052
  remaining -= granted;
836
1053
  });
837
- const truncated = [];
838
1054
  const budgeted = sources.map((source, index) => {
839
1055
  const limit = allowance.get(index) ?? 0;
840
1056
  if (source.text.length <= limit) return source;
841
1057
  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
- });
1058
+ record(index, source, text);
847
1059
  return { ...source, text };
848
1060
  });
849
- return { sources: budgeted, truncated };
1061
+ return { sources: budgeted, truncated: [...records.values()] };
850
1062
  }
851
1063
 
852
1064
  // src/coerce/validator.ts
@@ -862,7 +1074,7 @@ function entries(count) {
862
1074
  return `${count} ${count === 1 ? "entry" : "entries"}`;
863
1075
  }
864
1076
  function validateConstraints(value, constraints, path, issues) {
865
- const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern } = constraints;
1077
+ const { minLength, maxLength, minimum, maximum, minItems, maxItems, pattern, format } = constraints;
866
1078
  if (Array.isArray(value)) {
867
1079
  if (minItems !== void 0 && value.length < minItems) {
868
1080
  issues.push({
@@ -906,6 +1118,10 @@ function validateConstraints(value, constraints, path, issues) {
906
1118
  received: value
907
1119
  });
908
1120
  }
1121
+ if (format !== void 0) {
1122
+ const message = validateFormat(value, format);
1123
+ if (message) issues.push({ path, message, received: value });
1124
+ }
909
1125
  return;
910
1126
  }
911
1127
  if (typeof value === "number") {
@@ -1247,8 +1463,15 @@ function generateSpanId() {
1247
1463
  }
1248
1464
  var Tracer = class {
1249
1465
  sinks;
1250
- constructor(sinks) {
1466
+ baseAttributes;
1467
+ /**
1468
+ * `baseAttributes` are merged into every span this tracer opens — how a
1469
+ * batch stamps `itemIndex` on the spans of each item, so a sink can tell
1470
+ * whose `llmCall` it is looking at under concurrency.
1471
+ */
1472
+ constructor(sinks, baseAttributes) {
1251
1473
  this.sinks = sinks ?? [];
1474
+ this.baseAttributes = baseAttributes;
1252
1475
  }
1253
1476
  startSpan(name, attributes, parent) {
1254
1477
  return {
@@ -1256,7 +1479,7 @@ var Tracer = class {
1256
1479
  name,
1257
1480
  startTime: Date.now(),
1258
1481
  events: [],
1259
- attributes,
1482
+ attributes: this.baseAttributes ? { ...this.baseAttributes, ...attributes } : attributes,
1260
1483
  parentId: parent?.id
1261
1484
  };
1262
1485
  }
@@ -1311,15 +1534,31 @@ async function resolveEnums(schema, bundle, enumResolver, tracer, parent) {
1311
1534
  tracer.endSpan(span);
1312
1535
  }
1313
1536
  }
1314
- async function runCoercion(input, options, { mode, provenance }) {
1315
- const { provider, schema, enumResolver, traceSinks } = options;
1316
- const bundle = options.bundle ?? bundleOf(schema);
1537
+ function emptyUsage() {
1538
+ return { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
1539
+ }
1540
+ function addUsage(into, usage) {
1541
+ into.calls += 1;
1542
+ if (!usage) return;
1543
+ into.promptTokens += usage.promptTokens;
1544
+ into.completionTokens += usage.completionTokens;
1545
+ into.totalTokens += usage.totalTokens;
1546
+ into.cacheReadTokens += usage.cacheReadTokens ?? 0;
1547
+ into.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
1548
+ }
1549
+ function checkOptions(options) {
1317
1550
  const maxRepairAttempts = options.maxRepairAttempts ?? 0;
1318
1551
  if (!Number.isInteger(maxRepairAttempts) || maxRepairAttempts < 0) {
1319
1552
  throw new RangeError(
1320
1553
  `maxRepairAttempts must be a non-negative integer, got ${String(options.maxRepairAttempts)}`
1321
1554
  );
1322
1555
  }
1556
+ const retryOnEmpty = options.retryOnEmpty ?? 0;
1557
+ if (!Number.isInteger(retryOnEmpty) || retryOnEmpty < 0) {
1558
+ throw new RangeError(
1559
+ `retryOnEmpty must be a non-negative integer, got ${String(options.retryOnEmpty)}`
1560
+ );
1561
+ }
1323
1562
  const instructions = normalizeInstructions(options.instructions);
1324
1563
  const onInvalidField = options.onInvalidField ?? "throw";
1325
1564
  if (!INVALID_FIELD_POLICIES.includes(onInvalidField)) {
@@ -1332,61 +1571,106 @@ async function runCoercion(input, options, { mode, provenance }) {
1332
1571
  `maxInputChars must be a positive integer, got ${String(options.maxInputChars)}`
1333
1572
  );
1334
1573
  }
1574
+ return { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions };
1575
+ }
1576
+ async function prepareRequest(options, { mode, provenance }, instructions, sourceLabels, tracer, rootSpan) {
1577
+ const { schema, enumResolver } = options;
1578
+ const bundle = options.bundle ?? bundleOf(schema);
1579
+ const resolvedEnums = await resolveEnums(schema, bundle, enumResolver, tracer, rootSpan);
1580
+ const promptSpan = tracer.startSpan("buildPrompt", {}, rootSpan);
1581
+ const basePrompt = buildPrompt(schema, bundle, { resolvedEnums, instructions });
1582
+ const provenanceOptions = { sourceLabels, fields: options.provenanceFields };
1583
+ const systemPrompt = provenance ? `${basePrompt}
1584
+ ${provenanceInstructions(provenanceOptions)}` : basePrompt;
1585
+ tracer.addEvent(promptSpan, "promptBuilt", {
1586
+ promptLength: systemPrompt.length,
1587
+ instructionCount: instructions.length,
1588
+ mode
1589
+ });
1590
+ tracer.endSpan(promptSpan);
1591
+ const request = provenance ? toProvenanceSchema(schema, bundle, provenanceOptions) : { schema, bundle };
1592
+ const schemaSpan = tracer.startSpan("buildJsonSchema", {}, rootSpan);
1593
+ const jsonSchema = runtimeSchemaToJsonSchema(request.schema, request.bundle, {
1594
+ resolvedEnums
1595
+ });
1596
+ tracer.endSpan(schemaSpan);
1597
+ return { systemPrompt, jsonSchema, schema: request.schema, bundle: request.bundle, resolvedEnums };
1598
+ }
1599
+ function isEmptyResult(data) {
1600
+ return Object.values(data).every((value) => value === null || value === void 0);
1601
+ }
1602
+ 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.";
1603
+ async function runCoercion(input, options, { mode, provenance, traceAttributes }) {
1604
+ const { provider, schema, traceSinks } = options;
1605
+ const bundle = options.bundle ?? bundleOf(schema);
1606
+ const { maxRepairAttempts, retryOnEmpty, onInvalidField, instructions } = checkOptions(options);
1335
1607
  const rawSources = toSources(input);
1336
- const tracer = new Tracer(traceSinks);
1608
+ const tracer = new Tracer(traceSinks, traceAttributes);
1337
1609
  const rootSpan = tracer.startSpan(mode, {
1338
1610
  schemaId: schema.id,
1339
1611
  provenance,
1340
1612
  onInvalidField,
1341
1613
  sourceCount: rawSources.length
1342
1614
  });
1615
+ const usage = emptyUsage();
1343
1616
  try {
1344
1617
  const sources = await prepareSources(rawSources, options, tracer, rootSpan);
1345
1618
  const sourceLabels = sources.length > 1 ? sources.map((s) => s.label ?? "") : [];
1346
- const resolvedEnums = await resolveEnums(
1347
- schema,
1348
- bundle,
1349
- enumResolver,
1619
+ const prepared = await prepareRequest(
1620
+ options,
1621
+ { mode, provenance },
1622
+ instructions,
1623
+ sourceLabels,
1350
1624
  tracer,
1351
1625
  rootSpan
1352
1626
  );
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);
1627
+ const { systemPrompt, jsonSchema, resolvedEnums } = prepared;
1368
1628
  const validate = mode === "coerce" ? validateStrict : validatePartial;
1369
1629
  const renderedInput = renderSources(sources);
1630
+ const hasInput = sources.some((s) => s.text.trim().length > 0);
1370
1631
  tracer.addEvent(rootSpan, "inputRendered", {
1371
1632
  sourceCount: sources.length,
1372
1633
  inputLength: renderedInput.length
1373
1634
  });
1374
1635
  let userInput = renderedInput;
1375
1636
  let issues = [];
1376
- let run = { data: {}, provenance: {}, issues: [] };
1637
+ let run = { data: {}, provenance: {}, issues: [], usage };
1638
+ let emptyRetries = 0;
1639
+ const multiTurn = provider.supportsHistory === true;
1640
+ const history = [];
1641
+ const followUp = (rejected, text, folded) => {
1642
+ if (multiTurn) {
1643
+ history.push({ role: "assistant", data: rejected }, { role: "user", text });
1644
+ } else {
1645
+ userInput = folded;
1646
+ }
1647
+ };
1377
1648
  for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
1378
- const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
1649
+ const llmSpan = tracer.startSpan("llmCall", { attempt, turns: history.length }, rootSpan);
1379
1650
  const response = await provider.complete({
1380
1651
  systemPrompt,
1381
1652
  userInput,
1653
+ ...history.length > 0 ? { history: [...history] } : {},
1382
1654
  jsonSchema,
1383
- schema: request.schema,
1384
- bundle: request.bundle,
1655
+ schema: prepared.schema,
1656
+ bundle: prepared.bundle,
1385
1657
  resolvedEnums
1386
1658
  });
1659
+ addUsage(usage, response.usage);
1387
1660
  tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1388
1661
  tracer.endSpan(llmSpan);
1389
- run = provenance ? { ...splitProvenance(response.data, schema), issues: [] } : { data: response.data, provenance: {}, issues: [] };
1662
+ run = provenance ? { ...splitProvenance(response.data, schema), issues: [], usage } : { data: response.data, provenance: {}, issues: [], usage };
1663
+ if (hasInput && emptyRetries < retryOnEmpty && isEmptyResult(run.data)) {
1664
+ emptyRetries += 1;
1665
+ tracer.addEvent(rootSpan, "emptyRetry", { retry: emptyRetries });
1666
+ followUp(response.data, EMPTY_RETRY_NOTE, `${renderedInput}
1667
+
1668
+ ---
1669
+
1670
+ ${EMPTY_RETRY_NOTE}`);
1671
+ attempt -= 1;
1672
+ continue;
1673
+ }
1390
1674
  const validationSpan = tracer.startSpan("validate", { attempt }, rootSpan);
1391
1675
  issues = validate(run.data, schema, bundle, { resolvedEnums });
1392
1676
  tracer.addEvent(validationSpan, "validated", { issueCount: issues.length });
@@ -1412,7 +1696,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1412
1696
  return {
1413
1697
  data: outcome.data,
1414
1698
  provenance: pruneProvenance(run.provenance, outcome.resolved),
1415
- issues: outcome.resolved
1699
+ issues: outcome.resolved,
1700
+ usage
1416
1701
  };
1417
1702
  }
1418
1703
  }
@@ -1423,7 +1708,11 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1423
1708
  issueCount: issues.length,
1424
1709
  paths: issues.map((issue) => issue.path)
1425
1710
  });
1426
- userInput = buildRepairInput(renderedInput, run.data, issues);
1711
+ followUp(
1712
+ response.data,
1713
+ buildRepairCorrection(issues),
1714
+ buildRepairInput(renderedInput, run.data, issues)
1715
+ );
1427
1716
  }
1428
1717
  }
1429
1718
  throw new CoerceError(issues);
@@ -1433,7 +1722,8 @@ ${provenanceInstructions({ sourceLabels })}` : basePrompt;
1433
1722
  }
1434
1723
  async function prepareSources(sources, options, tracer, parent) {
1435
1724
  const { preprocess, maxInputChars, truncate } = options;
1436
- if (!preprocess && maxInputChars === void 0) {
1725
+ const anyCapped = sources.some((s) => s.maxChars !== void 0);
1726
+ if (!preprocess && maxInputChars === void 0 && !anyCapped) {
1437
1727
  return [...sources];
1438
1728
  }
1439
1729
  const span = tracer.startSpan("prepareInput", {}, parent);
@@ -1450,7 +1740,7 @@ async function prepareSources(sources, options, tracer, parent) {
1450
1740
  lengths: prepared.map((s) => s.text.length)
1451
1741
  });
1452
1742
  }
1453
- if (maxInputChars !== void 0) {
1743
+ if (maxInputChars !== void 0 || prepared.some((s) => s.maxChars !== void 0)) {
1454
1744
  const budgeted = budgetSources(prepared, maxInputChars, truncate);
1455
1745
  prepared = budgeted.sources;
1456
1746
  if (budgeted.truncated.length > 0) {
@@ -1498,19 +1788,59 @@ async function partialCoerce(input, options) {
1498
1788
  });
1499
1789
  return stripNulls(data);
1500
1790
  }
1791
+ async function coerceDetailed(input, options) {
1792
+ const { data, issues, usage } = await runCoercion(input, options, {
1793
+ mode: "coerce",
1794
+ provenance: false
1795
+ });
1796
+ return { data, issues, usage };
1797
+ }
1798
+ async function partialCoerceDetailed(input, options) {
1799
+ const { data, issues, usage } = await runCoercion(input, options, {
1800
+ mode: "partialCoerce",
1801
+ provenance: false
1802
+ });
1803
+ return { data: stripNulls(data), issues, usage };
1804
+ }
1805
+ var PRIME_INPUT = "Cache warm-up. There is no input to extract from; return an object with every field null.";
1806
+ async function primeCache(options) {
1807
+ const { mode = "coerce", provenance = false, ...coerceOptions } = options;
1808
+ const { instructions } = checkOptions(coerceOptions);
1809
+ const tracer = new Tracer(coerceOptions.traceSinks);
1810
+ const rootSpan = tracer.startSpan("primeCache", { schemaId: coerceOptions.schema.id, mode, provenance });
1811
+ const usage = emptyUsage();
1812
+ try {
1813
+ const prepared = await prepareRequest(coerceOptions, { mode, provenance }, instructions, [], tracer, rootSpan);
1814
+ const llmSpan = tracer.startSpan("llmCall", { attempt: 0, warmup: true }, rootSpan);
1815
+ const response = await coerceOptions.provider.complete({
1816
+ systemPrompt: prepared.systemPrompt,
1817
+ userInput: renderSources(toSources(PRIME_INPUT)),
1818
+ jsonSchema: prepared.jsonSchema,
1819
+ schema: prepared.schema,
1820
+ bundle: prepared.bundle,
1821
+ resolvedEnums: prepared.resolvedEnums
1822
+ });
1823
+ addUsage(usage, response.usage);
1824
+ tracer.addEvent(llmSpan, "responseReceived", { usage: response.usage });
1825
+ tracer.endSpan(llmSpan);
1826
+ return { schemaId: coerceOptions.schema.id, mode, provenance, usage, primedAt: (/* @__PURE__ */ new Date()).toISOString() };
1827
+ } finally {
1828
+ tracer.endSpan(rootSpan);
1829
+ }
1830
+ }
1501
1831
  async function coerceWithProvenance(input, options) {
1502
- const { data, provenance, issues } = await runCoercion(input, options, {
1832
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1503
1833
  mode: "coerce",
1504
1834
  provenance: true
1505
1835
  });
1506
- return { data, provenance, issues };
1836
+ return { data, provenance, issues, usage };
1507
1837
  }
1508
1838
  async function partialCoerceWithProvenance(input, options) {
1509
- const { data, provenance, issues } = await runCoercion(input, options, {
1839
+ const { data, provenance, issues, usage } = await runCoercion(input, options, {
1510
1840
  mode: "partialCoerce",
1511
1841
  provenance: true
1512
1842
  });
1513
- return { data: stripNulls(data), provenance, issues };
1843
+ return { data: stripNulls(data), provenance, issues, usage };
1514
1844
  }
1515
1845
 
1516
1846
  // src/coerce/coerce-many.ts
@@ -1551,60 +1881,107 @@ var BackoffGate = class {
1551
1881
  this.streak = 0;
1552
1882
  }
1553
1883
  };
1884
+ var InputQueue = class {
1885
+ iterator;
1886
+ pulling = Promise.resolve();
1887
+ index = 0;
1888
+ constructor(inputs) {
1889
+ this.iterator = Symbol.asyncIterator in inputs ? inputs[Symbol.asyncIterator]() : inputs[Symbol.iterator]();
1890
+ }
1891
+ next() {
1892
+ const pull = this.pulling.then(async () => {
1893
+ const result = await this.iterator.next();
1894
+ if (result.done) return void 0;
1895
+ return { index: this.index++, input: result.value };
1896
+ });
1897
+ this.pulling = pull.catch(() => void 0);
1898
+ return pull;
1899
+ }
1900
+ };
1901
+ function labelOf(input) {
1902
+ if (typeof input === "string") return void 0;
1903
+ if (isSource(input)) return input.label;
1904
+ return input[0]?.label;
1905
+ }
1554
1906
  async function coerceMany(inputs, options) {
1555
1907
  const {
1556
1908
  concurrency = DEFAULT_CONCURRENCY,
1557
1909
  mode = "coerce",
1558
- provenance = false,
1559
- primeCache = true,
1910
+ provenance: provenanceOption = false,
1911
+ primeCache: primeCache2 = true,
1912
+ primed,
1560
1913
  onItem,
1561
1914
  signal,
1915
+ retry: retryOptions,
1562
1916
  ...coerceOptions
1563
1917
  } = options;
1564
- const retry = { ...DEFAULT_RETRY, ...options.retry };
1918
+ const retry = { ...DEFAULT_RETRY, ...retryOptions };
1919
+ const provenance = provenanceOption !== false;
1920
+ if (Array.isArray(provenanceOption)) {
1921
+ coerceOptions.provenanceFields = provenanceOption;
1922
+ }
1565
1923
  if (!Number.isInteger(concurrency) || concurrency < 1) {
1566
1924
  throw new RangeError(`concurrency must be a positive integer, got ${String(concurrency)}`);
1567
1925
  }
1568
1926
  if (!Number.isInteger(retry.attempts) || retry.attempts < 0) {
1569
1927
  throw new RangeError(`retry.attempts must be a non-negative integer, got ${String(retry.attempts)}`);
1570
1928
  }
1571
- const results = new Array(inputs.length);
1929
+ const results = [];
1572
1930
  const gate = new BackoffGate(retry);
1573
- async function runOne(index) {
1931
+ const queue = new InputQueue(inputs);
1932
+ async function runOne(index, input) {
1574
1933
  let attempts = 0;
1575
1934
  let result;
1935
+ let usage = emptyUsage();
1936
+ const label = labelOf(input);
1937
+ const traceAttributes = { itemIndex: index, ...label !== void 0 ? { itemLabel: label } : {} };
1576
1938
  for (; ; ) {
1577
1939
  if (signal?.aborted) {
1578
- result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), attempts };
1940
+ result = { ok: false, index, error: signal.reason ?? new Error("Batch aborted"), usage, attempts };
1579
1941
  break;
1580
1942
  }
1581
1943
  await gate.wait();
1582
1944
  attempts += 1;
1583
1945
  try {
1584
- const run = await runCoercion(inputs[index], coerceOptions, { mode, provenance });
1946
+ const run = await runCoercion(input, coerceOptions, { mode, provenance, traceAttributes });
1585
1947
  const data = mode === "partialCoerce" ? stripNulls(run.data) : run.data;
1586
1948
  gate.succeeded();
1587
- result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, attempts };
1949
+ result = { ok: true, index, data, provenance: run.provenance, issues: run.issues, usage: run.usage, attempts };
1588
1950
  break;
1589
1951
  } catch (error) {
1952
+ usage = emptyUsage();
1590
1953
  if (isRetryable(error) && attempts <= retry.attempts) {
1591
1954
  gate.failed();
1592
1955
  continue;
1593
1956
  }
1594
- result = { ok: false, index, error, attempts };
1957
+ result = { ok: false, index, error, usage, attempts };
1595
1958
  break;
1596
1959
  }
1597
1960
  }
1598
1961
  results[index] = result;
1599
1962
  onItem?.(result);
1600
1963
  }
1601
- let next = 0;
1602
- if (primeCache && inputs.length > 1) {
1603
- await runOne(next++);
1964
+ let pending;
1965
+ const warmup = primed ?? (primeCache2 === "eager" ? primeCache({ ...coerceOptions, mode, provenance }) : void 0);
1966
+ if (warmup) {
1967
+ await Promise.resolve(warmup).catch(() => void 0);
1968
+ } else if (primeCache2 === true) {
1969
+ const first = await queue.next();
1970
+ if (first === void 0) return results;
1971
+ const second = await queue.next();
1972
+ if (second === void 0) {
1973
+ await runOne(first.index, first.input);
1974
+ return results;
1975
+ }
1976
+ await runOne(first.index, first.input);
1977
+ pending = second;
1604
1978
  }
1605
- const workers = Array.from({ length: Math.min(concurrency, inputs.length) }, async () => {
1606
- while (next < inputs.length) {
1607
- await runOne(next++);
1979
+ const workers = Array.from({ length: concurrency }, async () => {
1980
+ for (; ; ) {
1981
+ const item = pending ?? await queue.next();
1982
+ pending = void 0;
1983
+ if (item === void 0) return;
1984
+ await runOne(item.index, item.input);
1608
1985
  }
1609
1986
  });
1610
1987
  await Promise.all(workers);
@@ -1643,6 +2020,7 @@ function resolveConfig(callConfig) {
1643
2020
  maxRepairAttempts: callConfig?.maxRepairAttempts ?? global.maxRepairAttempts,
1644
2021
  onInvalidField: callConfig?.onInvalidField ?? global.onInvalidField,
1645
2022
  instructions: callConfig?.instructions ?? global.instructions,
2023
+ retryOnEmpty: callConfig?.retryOnEmpty ?? global.retryOnEmpty,
1646
2024
  maxInputChars: callConfig?.maxInputChars ?? global.maxInputChars,
1647
2025
  truncate: callConfig?.truncate ?? global.truncate,
1648
2026
  preprocess: callConfig?.preprocess ?? global.preprocess
@@ -1677,6 +2055,7 @@ var Coercible = class _Coercible {
1677
2055
  maxRepairAttempts: this._config.maxRepairAttempts,
1678
2056
  onInvalidField: this._config.onInvalidField,
1679
2057
  instructions: this._config.instructions,
2058
+ retryOnEmpty: this._config.retryOnEmpty,
1680
2059
  maxInputChars: this._config.maxInputChars,
1681
2060
  truncate: this._config.truncate,
1682
2061
  preprocess: this._config.preprocess
@@ -1743,6 +2122,7 @@ var ConsoleSink = class {
1743
2122
  Constrain,
1744
2123
  Describe,
1745
2124
  EnumResolutionError,
2125
+ FIELD_FORMATS,
1746
2126
  PROVENANCE_INSTRUCTIONS,
1747
2127
  SOURCE_INSTRUCTIONS,
1748
2128
  Schema,
@@ -1752,19 +2132,25 @@ var ConsoleSink = class {
1752
2132
  ValuesFrom,
1753
2133
  budgetSources,
1754
2134
  buildPrompt,
2135
+ buildRepairCorrection,
1755
2136
  buildRepairInput,
1756
2137
  bundleOf,
1757
2138
  coerce,
2139
+ coerceDetailed,
1758
2140
  coerceMany,
1759
2141
  coerceWithProvenance,
1760
2142
  collectEnumSources,
1761
2143
  defineSchema,
2144
+ describeFormat,
1762
2145
  field,
2146
+ formatToJsonSchema,
1763
2147
  isCoerceInput,
1764
2148
  isSource,
1765
2149
  normalizeInstructions,
1766
2150
  partialCoerce,
2151
+ partialCoerceDetailed,
1767
2152
  partialCoerceWithProvenance,
2153
+ primeCache,
1768
2154
  provenanceInstructions,
1769
2155
  renderSources,
1770
2156
  resolveEnumSources,
@@ -1775,6 +2161,7 @@ var ConsoleSink = class {
1775
2161
  toOpenAIJsonSchema,
1776
2162
  toProvenanceSchema,
1777
2163
  toSources,
2164
+ validateFormat,
1778
2165
  validatePartial,
1779
2166
  validateStrict
1780
2167
  });