@sembl/core 0.4.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.d.cts CHANGED
@@ -119,30 +119,28 @@ interface SchemaBundle {
119
119
  }
120
120
 
121
121
  /**
122
- * Supplies the legal values for a `dynamicEnum` source at coercion time.
123
- *
124
- * Sources are opaque to SEMBL: the id is whatever the schema author wrote in
125
- * `@ValuesFrom("...")`, and the caller decides how to turn it into values (a
126
- * CMS fetch, a database query, a static map). Called at most once per distinct
127
- * source id per coercion; the caller owns any caching across coercions.
128
- *
129
- * ```ts
130
- * const enumResolver: EnumResolver = async (sourceId) => {
131
- * const docs = await cms.taxonomy(sourceId);
132
- * return docs.map((d) => d.slug);
133
- * };
134
- * ```
122
+ * What a resolver is told about the source it is asked for, beyond its id:
123
+ * which schema is being coerced and where in it the source is used. One
124
+ * resolver can then serve several taxonomies, log which field asked, or
125
+ * refuse a source that a required field depends on but it cannot vouch for.
135
126
  */
136
- type EnumResolver = (sourceId: string) => readonly string[] | Promise<readonly string[]>;
127
+ interface EnumResolverContext {
128
+ sourceId: string;
129
+ /** The schema being coerced — the root, not a nested one. */
130
+ schema: RuntimeSchema;
131
+ /** Whether a chain of required fields reaches the source. */
132
+ required: boolean;
133
+ /** Dotted paths of every field drawing from the source, e.g. `address.country`. */
134
+ paths: string[];
135
+ }
137
136
  /**
138
- * The legal values for every enum source that resolved successfully,
139
- * keyed by source id.
140
- *
141
- * A source id absent from this map is *unresolved* — the field it backs falls
142
- * back to a free-form string. Successful resolution always yields a non-empty
143
- * array; an empty result is treated as a failure, not as "no legal values",
144
- * because a field with zero legal values is unsatisfiable.
137
+ * Resolves the legal values of a `@ValuesFrom` source at coercion time.
138
+ * Called once per distinct source id per coercion; caching is the caller's.
139
+ * The context argument is optional to accept — a resolver that only needs
140
+ * the id can ignore it.
145
141
  */
142
+ type EnumResolver = (sourceId: string, context: EnumResolverContext) => readonly string[] | Promise<readonly string[]>;
143
+ /** Resolved values keyed by source id. */
146
144
  type ResolvedEnums = Readonly<Record<string, readonly string[]>>;
147
145
 
148
146
  type JsonSchema = Record<string, unknown>;
@@ -469,14 +467,35 @@ interface ProviderConfig {
469
467
  /** Optional max tokens for the response */
470
468
  maxTokens?: number;
471
469
  }
470
+ /**
471
+ * One earlier turn of a repair conversation. An assistant turn is the
472
+ * structured output the model produced; a user turn is what was said about
473
+ * it. Providers render them natively — as a tool call and its result, or as
474
+ * assistant and user messages — so the model sees its own rejected answer
475
+ * as its own rather than quoted back to it.
476
+ */
477
+ type ProviderTurn = {
478
+ role: "assistant";
479
+ data: Record<string, unknown>;
480
+ } | {
481
+ role: "user";
482
+ text: string;
483
+ };
472
484
  /**
473
485
  * Request sent to a provider for structured output.
474
486
  */
475
487
  interface ProviderRequest {
476
488
  /** System prompt with semantic context */
477
489
  systemPrompt: string;
478
- /** User input to coerce */
490
+ /** User input to coerce — the first user turn of the conversation. */
479
491
  userInput: string;
492
+ /**
493
+ * Turns after `userInput`, in order, for a repair or an empty-result
494
+ * retry: the rejected output as an assistant turn, then the correction as
495
+ * a user turn, and so on. Only sent to a provider whose `supportsHistory`
496
+ * is true; other providers get the correction folded into `userInput`.
497
+ */
498
+ history?: ProviderTurn[];
480
499
  /** JSON Schema for structured output */
481
500
  jsonSchema: Record<string, unknown>;
482
501
  /** The runtime schema being targeted */
@@ -543,6 +562,12 @@ interface Provider {
543
562
  * Send a structured output request to the LLM.
544
563
  */
545
564
  complete(request: ProviderRequest): Promise<ProviderResponse>;
565
+ /**
566
+ * Whether `complete` renders `request.history` as real turns. Leave unset
567
+ * (false) and repair corrections arrive as text inside `userInput`
568
+ * instead, which every provider can handle.
569
+ */
570
+ readonly supportsHistory?: boolean;
546
571
  }
547
572
 
548
573
  /**
@@ -1138,6 +1163,12 @@ declare function coerceMany<T>(inputs: CoerceManyInputs, options: CoerceManyOpti
1138
1163
  * different wording can build their own and call the provider directly.
1139
1164
  */
1140
1165
  declare function buildRepairInput(originalInput: string, rejected: Record<string, unknown>, issues: FieldValidationIssue[]): string;
1166
+ /**
1167
+ * The correction alone — what was wrong and what to do — for a provider that
1168
+ * carries the rejected output as a real assistant turn, so the model does not
1169
+ * need it quoted back.
1170
+ */
1171
+ declare function buildRepairCorrection(issues: FieldValidationIssue[]): string;
1141
1172
 
1142
1173
  /**
1143
1174
  * Configuration options shared by global and per-call config.
@@ -1356,4 +1387,4 @@ declare class ConsoleSink implements TraceSink {
1356
1387
  write(span: TraceSpan): void;
1357
1388
  }
1358
1389
 
1359
- export { type BudgetResult, type CoerceDetails, CoerceError, type CoerceInput, type CoerceManyOptions, type CoerceManyResult, type CoerceOptions, type CoerceUsage, Coercible, ConsoleSink, Constrain, type DeepPartial, type DefinedSchema, Describe, type EnumResolution, EnumResolutionError, type EnumResolver, type EnumSourceFailure, type EnumSourceUsage, FIELD_FORMATS, type FieldBuilder, type FieldConfidence, type FieldConstraints, type FieldDescriptor, type FieldFormat, type FieldProvenance, type FieldType, type FieldValidationIssue, type Infer, type InferFields, type InvalidFieldPolicy, type IssueResolution, type JsonSchemaDialect, type JsonSchemaOptions, PROVENANCE_INSTRUCTIONS, type PreprocessSource, type PrimeCacheOptions, type PrimedPrefix, type PromptOptions, type ProvenanceOptions, type ProvenanceResult, type Provider, type ProviderConfig, type ProviderRequest, type ProviderResponse, type ProviderUsage, type ResolveIssuesOptions, type ResolveIssuesResult, type ResolvedEnums, type ResolvedIssue, type RetryOptions, type RuntimeSchema, SOURCE_INSTRUCTIONS, Schema, type SchemaBundle, SchemaRegistry, type SemblCallConfig, SemblConfig, type SemblGlobalConfig, type Source, type TraceContext, type TraceEvent, type TraceSink, type TraceSpan, Tracer, type TruncatePolicy, type TruncationRecord, type ValidationOptions, ValuesFrom, budgetSources, buildPrompt, buildRepairInput, bundleOf, coerce, coerceDetailed, coerceMany, coerceWithProvenance, collectEnumSources, defineSchema, describeFormat, field, formatToJsonSchema, isCoerceInput, isSource, normalizeInstructions, partialCoerce, partialCoerceDetailed, partialCoerceWithProvenance, primeCache, provenanceInstructions, renderSources, resolveEnumSources, resolveIssues, runtimeSchemaToJsonSchema, sembl, splitProvenance, toOpenAIJsonSchema, toProvenanceSchema, toSources, validateFormat, validatePartial, validateStrict };
1390
+ export { type BudgetResult, type CoerceDetails, CoerceError, type CoerceInput, type CoerceManyOptions, type CoerceManyResult, type CoerceOptions, type CoerceUsage, Coercible, ConsoleSink, Constrain, type DeepPartial, type DefinedSchema, Describe, type EnumResolution, EnumResolutionError, type EnumResolver, type EnumResolverContext, type EnumSourceFailure, type EnumSourceUsage, FIELD_FORMATS, type FieldBuilder, type FieldConfidence, type FieldConstraints, type FieldDescriptor, type FieldFormat, type FieldProvenance, type FieldType, type FieldValidationIssue, type Infer, type InferFields, type InvalidFieldPolicy, type IssueResolution, type JsonSchemaDialect, type JsonSchemaOptions, PROVENANCE_INSTRUCTIONS, type PreprocessSource, type PrimeCacheOptions, type PrimedPrefix, type PromptOptions, type ProvenanceOptions, type ProvenanceResult, type Provider, type ProviderConfig, type ProviderRequest, type ProviderResponse, type ProviderTurn, type ProviderUsage, type ResolveIssuesOptions, type ResolveIssuesResult, type ResolvedEnums, type ResolvedIssue, type RetryOptions, type RuntimeSchema, SOURCE_INSTRUCTIONS, Schema, type SchemaBundle, SchemaRegistry, type SemblCallConfig, SemblConfig, type SemblGlobalConfig, type Source, type TraceContext, type TraceEvent, type TraceSink, type TraceSpan, Tracer, type TruncatePolicy, type TruncationRecord, type ValidationOptions, ValuesFrom, budgetSources, buildPrompt, buildRepairCorrection, buildRepairInput, bundleOf, coerce, coerceDetailed, coerceMany, coerceWithProvenance, collectEnumSources, defineSchema, describeFormat, field, formatToJsonSchema, isCoerceInput, isSource, normalizeInstructions, partialCoerce, partialCoerceDetailed, partialCoerceWithProvenance, primeCache, provenanceInstructions, renderSources, resolveEnumSources, resolveIssues, runtimeSchemaToJsonSchema, sembl, splitProvenance, toOpenAIJsonSchema, toProvenanceSchema, toSources, validateFormat, validatePartial, validateStrict };
package/dist/index.d.ts CHANGED
@@ -119,30 +119,28 @@ interface SchemaBundle {
119
119
  }
120
120
 
121
121
  /**
122
- * Supplies the legal values for a `dynamicEnum` source at coercion time.
123
- *
124
- * Sources are opaque to SEMBL: the id is whatever the schema author wrote in
125
- * `@ValuesFrom("...")`, and the caller decides how to turn it into values (a
126
- * CMS fetch, a database query, a static map). Called at most once per distinct
127
- * source id per coercion; the caller owns any caching across coercions.
128
- *
129
- * ```ts
130
- * const enumResolver: EnumResolver = async (sourceId) => {
131
- * const docs = await cms.taxonomy(sourceId);
132
- * return docs.map((d) => d.slug);
133
- * };
134
- * ```
122
+ * What a resolver is told about the source it is asked for, beyond its id:
123
+ * which schema is being coerced and where in it the source is used. One
124
+ * resolver can then serve several taxonomies, log which field asked, or
125
+ * refuse a source that a required field depends on but it cannot vouch for.
135
126
  */
136
- type EnumResolver = (sourceId: string) => readonly string[] | Promise<readonly string[]>;
127
+ interface EnumResolverContext {
128
+ sourceId: string;
129
+ /** The schema being coerced — the root, not a nested one. */
130
+ schema: RuntimeSchema;
131
+ /** Whether a chain of required fields reaches the source. */
132
+ required: boolean;
133
+ /** Dotted paths of every field drawing from the source, e.g. `address.country`. */
134
+ paths: string[];
135
+ }
137
136
  /**
138
- * The legal values for every enum source that resolved successfully,
139
- * keyed by source id.
140
- *
141
- * A source id absent from this map is *unresolved* — the field it backs falls
142
- * back to a free-form string. Successful resolution always yields a non-empty
143
- * array; an empty result is treated as a failure, not as "no legal values",
144
- * because a field with zero legal values is unsatisfiable.
137
+ * Resolves the legal values of a `@ValuesFrom` source at coercion time.
138
+ * Called once per distinct source id per coercion; caching is the caller's.
139
+ * The context argument is optional to accept — a resolver that only needs
140
+ * the id can ignore it.
145
141
  */
142
+ type EnumResolver = (sourceId: string, context: EnumResolverContext) => readonly string[] | Promise<readonly string[]>;
143
+ /** Resolved values keyed by source id. */
146
144
  type ResolvedEnums = Readonly<Record<string, readonly string[]>>;
147
145
 
148
146
  type JsonSchema = Record<string, unknown>;
@@ -469,14 +467,35 @@ interface ProviderConfig {
469
467
  /** Optional max tokens for the response */
470
468
  maxTokens?: number;
471
469
  }
470
+ /**
471
+ * One earlier turn of a repair conversation. An assistant turn is the
472
+ * structured output the model produced; a user turn is what was said about
473
+ * it. Providers render them natively — as a tool call and its result, or as
474
+ * assistant and user messages — so the model sees its own rejected answer
475
+ * as its own rather than quoted back to it.
476
+ */
477
+ type ProviderTurn = {
478
+ role: "assistant";
479
+ data: Record<string, unknown>;
480
+ } | {
481
+ role: "user";
482
+ text: string;
483
+ };
472
484
  /**
473
485
  * Request sent to a provider for structured output.
474
486
  */
475
487
  interface ProviderRequest {
476
488
  /** System prompt with semantic context */
477
489
  systemPrompt: string;
478
- /** User input to coerce */
490
+ /** User input to coerce — the first user turn of the conversation. */
479
491
  userInput: string;
492
+ /**
493
+ * Turns after `userInput`, in order, for a repair or an empty-result
494
+ * retry: the rejected output as an assistant turn, then the correction as
495
+ * a user turn, and so on. Only sent to a provider whose `supportsHistory`
496
+ * is true; other providers get the correction folded into `userInput`.
497
+ */
498
+ history?: ProviderTurn[];
480
499
  /** JSON Schema for structured output */
481
500
  jsonSchema: Record<string, unknown>;
482
501
  /** The runtime schema being targeted */
@@ -543,6 +562,12 @@ interface Provider {
543
562
  * Send a structured output request to the LLM.
544
563
  */
545
564
  complete(request: ProviderRequest): Promise<ProviderResponse>;
565
+ /**
566
+ * Whether `complete` renders `request.history` as real turns. Leave unset
567
+ * (false) and repair corrections arrive as text inside `userInput`
568
+ * instead, which every provider can handle.
569
+ */
570
+ readonly supportsHistory?: boolean;
546
571
  }
547
572
 
548
573
  /**
@@ -1138,6 +1163,12 @@ declare function coerceMany<T>(inputs: CoerceManyInputs, options: CoerceManyOpti
1138
1163
  * different wording can build their own and call the provider directly.
1139
1164
  */
1140
1165
  declare function buildRepairInput(originalInput: string, rejected: Record<string, unknown>, issues: FieldValidationIssue[]): string;
1166
+ /**
1167
+ * The correction alone — what was wrong and what to do — for a provider that
1168
+ * carries the rejected output as a real assistant turn, so the model does not
1169
+ * need it quoted back.
1170
+ */
1171
+ declare function buildRepairCorrection(issues: FieldValidationIssue[]): string;
1141
1172
 
1142
1173
  /**
1143
1174
  * Configuration options shared by global and per-call config.
@@ -1356,4 +1387,4 @@ declare class ConsoleSink implements TraceSink {
1356
1387
  write(span: TraceSpan): void;
1357
1388
  }
1358
1389
 
1359
- export { type BudgetResult, type CoerceDetails, CoerceError, type CoerceInput, type CoerceManyOptions, type CoerceManyResult, type CoerceOptions, type CoerceUsage, Coercible, ConsoleSink, Constrain, type DeepPartial, type DefinedSchema, Describe, type EnumResolution, EnumResolutionError, type EnumResolver, type EnumSourceFailure, type EnumSourceUsage, FIELD_FORMATS, type FieldBuilder, type FieldConfidence, type FieldConstraints, type FieldDescriptor, type FieldFormat, type FieldProvenance, type FieldType, type FieldValidationIssue, type Infer, type InferFields, type InvalidFieldPolicy, type IssueResolution, type JsonSchemaDialect, type JsonSchemaOptions, PROVENANCE_INSTRUCTIONS, type PreprocessSource, type PrimeCacheOptions, type PrimedPrefix, type PromptOptions, type ProvenanceOptions, type ProvenanceResult, type Provider, type ProviderConfig, type ProviderRequest, type ProviderResponse, type ProviderUsage, type ResolveIssuesOptions, type ResolveIssuesResult, type ResolvedEnums, type ResolvedIssue, type RetryOptions, type RuntimeSchema, SOURCE_INSTRUCTIONS, Schema, type SchemaBundle, SchemaRegistry, type SemblCallConfig, SemblConfig, type SemblGlobalConfig, type Source, type TraceContext, type TraceEvent, type TraceSink, type TraceSpan, Tracer, type TruncatePolicy, type TruncationRecord, type ValidationOptions, ValuesFrom, budgetSources, buildPrompt, buildRepairInput, bundleOf, coerce, coerceDetailed, coerceMany, coerceWithProvenance, collectEnumSources, defineSchema, describeFormat, field, formatToJsonSchema, isCoerceInput, isSource, normalizeInstructions, partialCoerce, partialCoerceDetailed, partialCoerceWithProvenance, primeCache, provenanceInstructions, renderSources, resolveEnumSources, resolveIssues, runtimeSchemaToJsonSchema, sembl, splitProvenance, toOpenAIJsonSchema, toProvenanceSchema, toSources, validateFormat, validatePartial, validateStrict };
1390
+ export { type BudgetResult, type CoerceDetails, CoerceError, type CoerceInput, type CoerceManyOptions, type CoerceManyResult, type CoerceOptions, type CoerceUsage, Coercible, ConsoleSink, Constrain, type DeepPartial, type DefinedSchema, Describe, type EnumResolution, EnumResolutionError, type EnumResolver, type EnumResolverContext, type EnumSourceFailure, type EnumSourceUsage, FIELD_FORMATS, type FieldBuilder, type FieldConfidence, type FieldConstraints, type FieldDescriptor, type FieldFormat, type FieldProvenance, type FieldType, type FieldValidationIssue, type Infer, type InferFields, type InvalidFieldPolicy, type IssueResolution, type JsonSchemaDialect, type JsonSchemaOptions, PROVENANCE_INSTRUCTIONS, type PreprocessSource, type PrimeCacheOptions, type PrimedPrefix, type PromptOptions, type ProvenanceOptions, type ProvenanceResult, type Provider, type ProviderConfig, type ProviderRequest, type ProviderResponse, type ProviderTurn, type ProviderUsage, type ResolveIssuesOptions, type ResolveIssuesResult, type ResolvedEnums, type ResolvedIssue, type RetryOptions, type RuntimeSchema, SOURCE_INSTRUCTIONS, Schema, type SchemaBundle, SchemaRegistry, type SemblCallConfig, SemblConfig, type SemblGlobalConfig, type Source, type TraceContext, type TraceEvent, type TraceSink, type TraceSpan, Tracer, type TruncatePolicy, type TruncationRecord, type ValidationOptions, ValuesFrom, budgetSources, buildPrompt, buildRepairCorrection, buildRepairInput, bundleOf, coerce, coerceDetailed, coerceMany, coerceWithProvenance, collectEnumSources, defineSchema, describeFormat, field, formatToJsonSchema, isCoerceInput, isSource, normalizeInstructions, partialCoerce, partialCoerceDetailed, partialCoerceWithProvenance, primeCache, provenanceInstructions, renderSources, resolveEnumSources, resolveIssues, runtimeSchemaToJsonSchema, sembl, splitProvenance, toOpenAIJsonSchema, toProvenanceSchema, toSources, validateFormat, validatePartial, validateStrict };
package/dist/index.js CHANGED
@@ -318,7 +318,7 @@ async function resolveEnumSources(schema, resolver, bundle) {
318
318
  await Promise.all(
319
319
  [...usages].map(async ([sourceId, usage]) => {
320
320
  try {
321
- const values = await resolver(sourceId);
321
+ const values = await resolver(sourceId, { sourceId, schema, ...usage });
322
322
  if (!values || values.length === 0) {
323
323
  failures.push({ sourceId, reason: "empty", ...usage });
324
324
  return;
@@ -756,7 +756,7 @@ function renderReceived(received) {
756
756
  return text.length > MAX_RECEIVED_LENGTH ? `${text.slice(0, MAX_RECEIVED_LENGTH)}\u2026 (truncated)` : text;
757
757
  }
758
758
  function buildRepairInput(originalInput, rejected, issues) {
759
- const lines = [
759
+ return [
760
760
  originalInput,
761
761
  "",
762
762
  "---",
@@ -765,9 +765,11 @@ function buildRepairInput(originalInput, rejected, issues) {
765
765
  "",
766
766
  JSON.stringify(rejected, null, 2),
767
767
  "",
768
- "It was rejected because:",
769
- ""
770
- ];
768
+ buildRepairCorrection(issues)
769
+ ].join("\n");
770
+ }
771
+ function buildRepairCorrection(issues) {
772
+ const lines = ["The output was rejected because:", ""];
771
773
  for (const issue of issues) {
772
774
  lines.push(`- ${issue.path}: ${issue.message} (received: ${renderReceived(issue.received)})`);
773
775
  }
@@ -1561,11 +1563,21 @@ async function runCoercion(input, options, { mode, provenance, traceAttributes }
1561
1563
  let issues = [];
1562
1564
  let run = { data: {}, provenance: {}, issues: [], usage };
1563
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
+ };
1564
1575
  for (let attempt = 0; attempt <= maxRepairAttempts; attempt++) {
1565
- const llmSpan = tracer.startSpan("llmCall", { attempt }, rootSpan);
1576
+ const llmSpan = tracer.startSpan("llmCall", { attempt, turns: history.length }, rootSpan);
1566
1577
  const response = await provider.complete({
1567
1578
  systemPrompt,
1568
1579
  userInput,
1580
+ ...history.length > 0 ? { history: [...history] } : {},
1569
1581
  jsonSchema,
1570
1582
  schema: prepared.schema,
1571
1583
  bundle: prepared.bundle,
@@ -1578,11 +1590,11 @@ async function runCoercion(input, options, { mode, provenance, traceAttributes }
1578
1590
  if (hasInput && emptyRetries < retryOnEmpty && isEmptyResult(run.data)) {
1579
1591
  emptyRetries += 1;
1580
1592
  tracer.addEvent(rootSpan, "emptyRetry", { retry: emptyRetries });
1581
- userInput = `${renderedInput}
1593
+ followUp(response.data, EMPTY_RETRY_NOTE, `${renderedInput}
1582
1594
 
1583
1595
  ---
1584
1596
 
1585
- ${EMPTY_RETRY_NOTE}`;
1597
+ ${EMPTY_RETRY_NOTE}`);
1586
1598
  attempt -= 1;
1587
1599
  continue;
1588
1600
  }
@@ -1623,7 +1635,11 @@ ${EMPTY_RETRY_NOTE}`;
1623
1635
  issueCount: issues.length,
1624
1636
  paths: issues.map((issue) => issue.path)
1625
1637
  });
1626
- userInput = buildRepairInput(renderedInput, run.data, issues);
1638
+ followUp(
1639
+ response.data,
1640
+ buildRepairCorrection(issues),
1641
+ buildRepairInput(renderedInput, run.data, issues)
1642
+ );
1627
1643
  }
1628
1644
  }
1629
1645
  throw new CoerceError(issues);
@@ -2042,6 +2058,7 @@ export {
2042
2058
  ValuesFrom,
2043
2059
  budgetSources,
2044
2060
  buildPrompt,
2061
+ buildRepairCorrection,
2045
2062
  buildRepairInput,
2046
2063
  bundleOf,
2047
2064
  coerce,