@sembl/core 0.2.1 → 0.4.0

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