Skip to main content
An adapter sits between a signature and the LM: it formats inputs into the prompt the model sees and parses the LM’s response back into typed outputs. The default adapter is ChatAdapter, and Predict drives it for you (via the IR interpreter), so you touch it directly only to inspect what the model sees.

What adapters do

ChatAdapter handles both formatting and parsing. It has one lane: every method is parameterized by an owned SignatureDef — the IR’s value-level signature — whether the def came from a derived Signature type (SignatureDef::of::<S>()) or from a loaded .dsrs program. There is no separate “static” formatting path.

ChatAdapter

Prompt structure

The system message has four parts (in this order):
  1. Field descriptions - Lists input/output fields with types and descriptions
  2. Field structure - Shows the marker format + type schemas
  3. Response instructions - “Respond with [[ ## field ## ]]…”
  4. Task description - Your instruction from the signature docstring (at the end)
Example system message (simplified):

Marker protocol

Fields are delimited by markers:
This protocol (inspired by DSPy) allows mixing natural language with structured output.

Type schemas

For complex types, the adapter renders full schemas with typesys::render: type_name produces the short inline label, and schema_block expands class field layouts and enum value lists:
Type names are simplified for readability:
  • Module paths stripped: my_crate::SentimentSentiment
  • class/enum prefixes removed
  • " | "" or " (more natural)

Tolerant parsing with typesys::coerce

Response parsing uses typesys::coerce, a tolerant coercer that turns raw LM field text into typed values, guided by the expected field type. It handles:
  • Markdown fences - Strips ```json fences before parsing
  • Lists from prose - Parses bulleted or numbered lists into arrays
  • Type coercion - "42"42, "yes"true (loose bool and number spellings)
  • Surrounding text - Ignores extra prose around a JSON object or array
  • Partial recovery - Per-field failures aggregate; you get every error plus what did parse
Each non-fatal fix (a stripped fence, a coerced scalar) is recorded as a Flag on the returned Coerced value.

Constraint evaluation

After parsing, the adapter runs constraints:
  • #[check] results → stored in metadata
  • #[assert] failures → returns ParseError::AssertFailed

Error handling

Parse errors aggregate - you get all problems, not just the first:

Using the adapter directly

Usually you do not touch the adapter - Predict handles it. The public surface is five building blocks, all parameterized by &SignatureDef: For debugging what the LM sees from a derived signature:

Input formatting options

Input fields support two rendering paths:
  • #[format("json" | "yaml" | "toon")]: serialize a field using that format.
  • #[render(jinja = "...")]: render a field with a MiniJinja template.
  • #[format] and #[render] are mutually exclusive on the same field.
#[render] context in ChatAdapter:
  • this: current field value
  • input: full input object (plus top-level alias overlays)
  • field: { name, rust_name, type }
  • vars: adapter/surface vars (currently {})
ChatAdapter configures MiniJinja with strict undefined behavior. Available filters include:
  • MiniJinja built-ins
  • regex_match and sum helpers
  • truncate for length-limited string rendering
If template rendering fails at runtime (for example, missing variables), ChatAdapter panics. Templates are compiled per call — defs own their template strings, so there is no process-global template cache to leak per loaded program.

Real example: Insurance claim extraction

This is what a prompt looks like for a complex nested type (from examples/16-insurance-claim-prompt.rs):
Generated system message:
Generated user message:
Notice how:
  • Enums with many variants become “Definitions” at the top
  • Doc comments become inline // comments in the schema
  • Option<T> renders as T or null
  • Nested objects show their full structure
  • The instruction comes from the signature docstring

Design notes

  • DSPy inspiration: Marker protocol from DSPy’s ChatAdapter
  • BAML heritage: Schema rendering and tolerant parsing were originally built on the vendored BAML stack (jsonish and its schema renderer). The in-house typesys module (typesys::render, typesys::coerce) replaced that stack while keeping the same prompt-facing schema style.
  • Separation of concerns: Adapter does not know about LMs - just formatting/parsing

See also