ChatAdapter, and Predict drives it for you, so you touch it directly only to inspect what the model sees.
What adapters do
ChatAdapter handles both formatting and parsing.
ChatAdapter
Prompt structure
The system message has four parts (in this order):- Field descriptions - Lists input/output fields with types and descriptions
- Field structure - Shows the marker format + type schemas
- Response instructions - “Respond with
[[ ## field ## ]]…” - Task description - Your instruction from the signature docstring (at the end)
Marker protocol
Fields are delimited by markers:Type schemas
For complex types, the adapter renders full schemas withtypesys::render: type_name produces the short inline label, and schema_block expands class field layouts and enum value lists:
- Module paths stripped:
my_crate::Sentiment→Sentiment class/enumprefixes removed" | "→" or "(more natural)
Tolerant parsing with typesys::coerce
Response parsing usestypesys::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
Flag on the returned Coerced value.
Constraint evaluation
After parsing, the adapter runs constraints:#[check]results → stored in metadata#[assert]failures → returnsParseError::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. But for debugging:
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 valueinput: 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_matchandsumhelperstruncatefor length-limited string rendering
ChatAdapter panics.
Compiled templates are cached process-wide by template string.
Real example: Insurance claim extraction
This is what a prompt looks like for a complex nested type (fromexamples/16-insurance-claim-prompt.rs):
Full type definitions (InsuranceClaim and nested types)
Full type definitions (InsuranceClaim and nested types)
- Enums with many variants become “Definitions” at the top
- Doc comments become inline
//comments in the schema Option<T>renders asT 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 (
jsonishand its schema renderer). The in-housetypesysmodule (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
- Signatures for field types,
#[Schema], and constraints - Predict for the caller that drives the adapter
- LM for the client the formatted prompt is sent to
- Example: 16-insurance-claim-prompt.rs
