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