Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
62278e6
feat: multi-turn Predict + grouped Message model
Feb 20, 2026
eb92f42
fix: use new_spanned for stable/nightly-consistent trybuild snapshots
Feb 20, 2026
659c393
refactor: harden LM transcript fidelity, unify Predicted return shape…
darinkishore Feb 22, 2026
1d32cc3
Implement RLM v1 architecture and adapter/runtime deltas
Feb 25, 2026
621a13b
Preserve raw REPL output in submit feedback
Feb 25, 2026
367b65a
feat(rlm): merge PyO3 runtime modules into feature branch
Feb 25, 2026
0d03547
Fix passthrough input prompt ceremony leak
Feb 25, 2026
431e816
Wire RLM runtime defaults and OpenAI Responses live demo
Feb 26, 2026
1fc4d6a
Add V4 sub-LM budget interaction tests
Feb 26, 2026
3595c54
Harden RLM turn policy and error feedback paths
Feb 26, 2026
65edd81
Include bounded raw snippet in recoverable parse feedback
Feb 26, 2026
8729f46
Implement RLM V5 fallback extractor flow
Feb 26, 2026
6e4e808
Remove dead Xml chat adapter dialect
Feb 26, 2026
f3f0713
Add RLM sub-LM integration coverage
Feb 26, 2026
7f25f01
Deduplicate type-name formatting across adapters
Feb 26, 2026
db52a3c
rlm: tighten internals and unify type-name formatting
Feb 26, 2026
f00bb08
rlm: fix py_bridge path stack leakage on errors
Feb 26, 2026
fd2baac
rlm: remove redundant clones and dead submit/tool state
Feb 26, 2026
ad9cd00
rlm: migrate previews to peek-first renderer
Feb 26, 2026
774333b
rlm: restore media preview parity in peek renderer
Feb 26, 2026
e7f1980
rlm: harden tool runtime and reserved global names
Feb 26, 2026
3079788
rlm: remove unreachable preview fallback
Feb 26, 2026
c058bc9
rlm: implement Phase 2 injection and Phase 3 passthrough
Feb 26, 2026
cc5f153
Add rlm-derive macros for Phase 1 native RLM types
Feb 26, 2026
d8fa8a0
rlm: harden phase2 bridge contract
Feb 26, 2026
3f01518
rlm: implement Phase 4 preview renderer
Feb 26, 2026
8b6a428
rlm: add phase5 integration demo test
Feb 26, 2026
3a919c9
Wire LM additional_params and Anthropic prompt caching
Feb 27, 2026
f34e522
rlm: add tracing instrumentation to RLM module and previews
Feb 27, 2026
d638564
RLM prompt redesign: BAML schema renderer, structured system message,…
darinkishore Feb 28, 2026
71db0b2
RLM exec: strip markdown fences and leading prose before code execution
darinkishore Feb 28, 2026
ebfa07b
RLM loop: inject synthetic Turn 0 REPL demo before first model turn
darinkishore Feb 28, 2026
77fa8d7
RLM passthrough: execute all fenced code blocks
darinkishore Feb 28, 2026
263b68a
RLM fallback: preserve action-loop chat history
darinkishore Feb 28, 2026
7ca3304
RLM runtime: partial sub-LM batching, submit repair, doc newline pres…
darinkishore Feb 28, 2026
3dd45f9
RLM tools: clarify partial batch result alignment
darinkishore Feb 28, 2026
5c6ed00
Redesign RLM perception loop as flow-state REPL
darinkishore Feb 28, 2026
3c43e56
Lower output cap and add truncation var hints
darinkishore Feb 28, 2026
8df37da
REPL consonance: resource error formatting + situation-aware prompt
darinkishore Feb 28, 2026
5734678
REPL: namespace count in situation prompt
darinkishore Feb 28, 2026
cd46a7b
RLM: inject cleanup helper and document tool
darinkishore Feb 28, 2026
015d87e
Revert "RLM: inject cleanup helper and document tool"
darinkishore Feb 28, 2026
3c01bf7
REPL: collapse stable namespace to summary line
darinkishore Feb 28, 2026
9468537
RLM schema rendering overhaul: type dedup, nested methods, clean unions
darinkishore Mar 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 130 additions & 0 deletions INVESTIGATION_facet_baml_bridge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Investigation: Facet ↔ BAML Bridge Redundancy

## Summary

The codebase has **two independent paths** that produce BAML `TypeIR` from facet type metadata, causing divergence. BAML's native rendering is already used — the problem isn't that we're re-implementing rendering, it's that `SignatureSchema` builds its own TypeIR from raw `facet::Shape` while `bamltype`'s `SchemaBuilder` builds a richer, field-attr-aware TypeIR. These can disagree silently.

## The Two Paths (Root Problem)

### Path 1: `bamltype` SchemaBuilder (full-fidelity)
```
#[BamlType] struct → facet::Facet derive → facet attrs (bamltype::*)
→ SchemaBuilder::build_field_type_ir(field, owner, variant)
→ checks field attrs: with adapters, int_repr, map_key_repr
→ registers Classes/Enums into SchemaRegistry
→ builds OutputFormatContent (with recursive class detection)
→ cached in BamlSchema::baml_schema() as SchemaBundle
```
**Location:** `crates/bamltype/src/schema_builder.rs:453-490` (`build_field_type_ir`)

This path sees `#[baml(with="Codec")]`, `#[baml(int_repr="string")]`, `#[baml(map_key_repr="pairs")]` and transforms the TypeIR accordingly. An adapter can completely replace a field's type. A map can become a list of generated entry classes.

### Path 2: `SignatureSchema` (shape-only, loses field attrs)
```
#[derive(Signature)] struct → facet::Shape for Input/Output
→ collect_fields() iterates struct fields
→ emit_field() calls build_type_ir_from_shape(field.shape())
→ TypeIR built from shape alone, NO field attr awareness
→ stored in FieldSchema.type_ir
```
**Location:** `crates/dspy-rs/src/core/schema.rs:337` — the critical line:
```rust
let mut type_ir = build_type_ir_from_shape(field.shape());
```

This calls `schema_builder::build_type_ir_from_shape()` which creates a **fresh SchemaBuilder** and calls `build_type_ir(shape)` — NOT `build_field_type_ir(field, ...)`. It never sees field-level attributes.

### Where They're Used Together (Mismatch Surface)

| Consumer | Uses FieldSchema.type_ir (Path 2) | Uses OutputFormatContent (Path 1) |
|----------|----------------------------------|----------------------------------|
| `ChatAdapter::parse_structured_output_with_meta` | ✅ `jsonish::from_str(..., &field.type_ir, ...)` | ✅ `schema.output_format()` |
| RLM Output Contract (prompt.rs:99) | ✅ `field.type_ir.diagnostic_repr()` | ❌ |
| RLM py_bridge kwargs coercion | ✅ `field.type_ir` for dispatch | ✅ `output_format` for class/enum lookups |
| ChatAdapter field schema rendering | ❌ | ✅ `OutputFormatContent::render()` |

When Path 1 and Path 2 disagree (e.g., a field has `int_repr="string"` or `with="Codec"`), `jsonish` gets a TypeIR that says "int" while OutputFormatContent says "string" (or a completely different adapter type). This is a silent correctness bug.

## What BAML "Native" Actually Means Here

BAML's native rendering (`internal-baml-jinja`) is **already used**:
- `OutputFormatContent::render(options)` — schema prompt text ✅
- `jsonish::from_str(...)` — LLM output parsing ✅
- `format_baml_value(...)` — value formatting ✅

The custom bridge (`crates/bamltype`) builds the **inputs** to native rendering:
- `facet::Shape` → `TypeIR` (the type graph)
- `facet::Shape` → `OutputFormatContent` (class/enum registry)

You can't remove this bridge without a replacement source of truth (e.g., a BAML compiler, or user-authored BAML schemas).

## RlmType: Not a Schema Divergence

`#[rlm_type]` is a composition macro, not a competing schema system:
```rust
// rlm_attr.rs:43-45 — it literally just adds these:
input.attrs.push(syn::parse_quote!(#[pyclass(...)]));
input.attrs.push(syn::parse_quote!(#[BamlType]));
merge_derive(&mut input.attrs, &[syn::parse_quote!(RlmType)]);
```

`RlmType` derive adds Python interop methods (`__baml__`, `__repr__`, `__iter__`, etc.) that delegate to `BamlType` for conversion. There's no schema divergence here — it's a pure consumer of `bamltype`.

## Internal Name Drift

There's a subtle naming inconsistency between two functions that compute BAML internal names:

**`schema_builder::internal_name_for_shape(shape)`** (schema_builder.rs:44-55):
```rust
// Uses module_path::type_identifier
format!("{module}::{}", shape.type_identifier)
```

**`runtime::baml_internal_name::<T>()`** (runtime.rs:80-94):
```rust
// Falls back to std::any::type_name::<T>()
std::any::type_name::<T>()
```

`std::any::type_name` returns e.g. `my_crate::my_module::MyType` while `internal_name_for_shape` returns `my_module::MyType`. These could drift in edge cases, causing class lookup failures in value conversion or formatting.

## Complexity Hotspots

### 1. Adapter Function Pointers in Facet Attrs
`bamltype-derive` encodes function pointers (`WithAdapterFns`) into facet attribute metadata. These are `fn()` pointers stored as `&'static dyn Any` in compile-time reflection data. This works but is deeply non-obvious and makes the bridge hard to replace.

### 2. Map Key Repr "pairs" Generates Phantom Classes
`map_key_repr="pairs"` lowers `Map<K,V>` → `List<GeneratedMapEntry>` and registers a generated class. Any code that assumes maps stay maps will break.

### 3. Two Value Conversion Engines
- `bamltype/src/convert.rs`: Rust value ↔ BamlValue (facet Peek-based)
- `rlm/py_bridge.rs`: Python value → BamlValue (TypeIR + OutputFormatContent-aware)

Both walk value trees against schemas, both have relaxed parsing heuristics, both could diverge.

## Recommendations

### Fix 1: Make SignatureSchema source TypeIR from bamltype's SchemaBundle (HIGH PRIORITY)

Instead of:
```rust
let mut type_ir = build_type_ir_from_shape(field.shape());
```

Do one of:
- **Option A**: Look up the field's TypeIR from `<Output as BamlType>::baml_schema().output_format` class definitions
- **Option B**: Expose `SchemaBuilder::build_field_type_ir` as a public API that `SignatureSchema` can call

This eliminates the "two sources of truth" problem entirely.

### Fix 2: Unify internal name computation

Change `runtime::baml_internal_name::<T>()` fallback from `type_name::<T>()` to `internal_name_for_shape(T::SHAPE)`.

### Fix 3: Use OutputFormatContent::render for RLM Output Contract

Instead of `field.type_ir.diagnostic_repr()` (which uses the divergent Path 2 TypeIR), render the contract using the same native rendering used for structured output prompts.

### Fix 4 (Optional): Consolidate py_bridge coercion through jsonish

Normalize Python values to JSON, then use `jsonish::from_str(output_format, type_ir, ...)` instead of a parallel walker. Keeps one coercion engine.
6 changes: 6 additions & 0 deletions crates/dspy-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ enum_dispatch = "0.3.13"
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "fmt"] }
minijinja = { git = "https://github.com/boundaryml/minijinja.git", branch = "main", default-features = false, features = ["builtins", "serde"] }
pyo3 = { version = "0.27", features = ["auto-initialize"], optional = true }
rlm-derive = { path = "../rlm-derive", optional = true }

[package.metadata.cargo-machete]
ignored = ["rig-core"]

[features]
default = []
rlm = ["dep:pyo3", "dep:rlm-derive", "dsrs_macros/rlm"]

[dev-dependencies]
temp-env = { version = "0.3.6", features = ["async_closure"] }
14 changes: 9 additions & 5 deletions crates/dspy-rs/examples/01-simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ use anyhow::Result;
use bon::Builder;
use dspy_rs::data::RawExample;
use dspy_rs::{
CallMetadata, ChatAdapter, Example, LM, LmError, Module, Predict, PredictError, Predicted,
Prediction, configure, init_tracing,
CallMetadata, Chat, ChatAdapter, Example, LM, LmError, Module, Predict, PredictError,
Predicted, Prediction, configure, init_tracing,
};

const QA_INSTRUCTION: &str = "Answer the question step by step.";
Expand Down Expand Up @@ -115,7 +115,11 @@ impl Module for QARater {
.data
.insert("rating".into(), rate_result.rating.into());

Ok(Predicted::new(combined, CallMetadata::default()))
Ok(Predicted::new(
combined,
CallMetadata::default(),
Chat::new(vec![]),
))
}
}

Expand All @@ -128,7 +132,7 @@ async fn main() -> Result<()> {
.model("openai:gpt-4o-mini".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

// =========================================================================
Expand All @@ -147,7 +151,7 @@ async fn main() -> Result<()> {
println!("Reasoning: {}", output.reasoning);
println!("Answer: {}", output.answer);

// Predicted carries both typed output and metadata.
// Predicted carries typed output, metadata, and chat history.
let result = predict.call(input).await?;
println!("\nWith metadata:");
println!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async fn main() -> Result<()> {
.model("openai:gpt-4o-mini".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let metric = ExactMatch;
Expand Down
2 changes: 1 addition & 1 deletion crates/dspy-rs/examples/03-evaluate-hotpotqa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ async fn main() -> Result<()> {
.model("openai:gpt-4o-mini".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let examples = DataLoader::load_hf::<QA>(
Expand Down
2 changes: 1 addition & 1 deletion crates/dspy-rs/examples/04-optimize-hotpotqa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ async fn main() -> Result<()> {
.model("openai:gpt-4o-mini".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let examples = DataLoader::load_hf::<QA>(
Expand Down
2 changes: 1 addition & 1 deletion crates/dspy-rs/examples/05-heterogenous-examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async fn main() -> Result<()> {
.model("openai:gpt-4o-mini".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let heterogeneous = RawExample::new(
Expand Down
4 changes: 2 additions & 2 deletions crates/dspy-rs/examples/06-other-providers-batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async fn main() -> Result<()> {
.model("anthropic:claude-sonnet-4-5-20250929".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let mut anthropic = Vec::new();
Expand All @@ -63,7 +63,7 @@ async fn main() -> Result<()> {
.model("gemini:gemini-2.0-flash".to_string())
.build()
.await?,
ChatAdapter,
ChatAdapter::new(),
);

let mut gemini = Vec::new();
Expand Down
Loading
Loading