Skip to content
Prompting Press v0.2

Compose multi-message prompts

Composition is an explicit, ordered sequence of (Prompt, vars, variant?) entries that resolves to a Vec<Message> / list[Message] / Message[] in append order. It is the few-shot / system+user sequence builder.

There is no registry — each entry holds a Prompt object directly. There is deliberately no fluent .chain() API (it cannot cross the PyO3/napi FFI boundary and collides with Iterator::chain in Rust / standard JS idiom).

An entry is (prompt, vars, variant?) everywhere; only its spelling differs by language idiom: Rust and Python pass them as positional args to append (Python also accepts (prompt, vars[, variant]) tuples in from_messages), while TypeScript takes a single CompositionEntry object ({ prompt, schema?, data, variant? }) — matching how render is shaped in each language.

Validation is eager — at append time in Rust/Python, at append/fromMessages time in TypeScript. An invalid entry is rejected immediately and nothing is stored. resolve() therefore only ever sees fully-validated entries. If any entry fails at resolve (unknown variant, strict-undefined reference), the whole call fails and no partial result is returned.

guides_composition_build.rs
//! Build a Composition by appending (prompt, vars, variant?) entries, then
//! resolve it to an ordered Vec<Message>. Standalone:
//! `cargo run --example guides_composition_build`.
use garde::Validate;
use prompting_press::{Composition, Prompt, PromptDefinition};
use serde::Serialize;
#[derive(Serialize, Validate)]
struct SysVars {
#[garde(length(min = 1))]
instructions: String,
}
#[derive(Serialize, Validate)]
struct UserVars {
#[garde(length(min = 1))]
query: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build the two prompts inline from their shape, so the content is explicit.
let sys_def: PromptDefinition = serde_json::from_value(serde_json::json!({
"name": "system-preamble",
"role": "system",
"body": "{{ instructions }}",
"variables": { "instructions": { "type": "string", "trusted": true } }
}))?;
let user_def: PromptDefinition = serde_json::from_value(serde_json::json!({
"name": "user-turn",
"role": "user",
"body": "{{ query }}",
"variables": { "query": { "type": "string", "trusted": false } }
}))?;
let sys = Prompt::new(sys_def)?;
let user = Prompt::new(user_def)?;
let mut comp = Composition::new();
comp.append(
&sys,
&SysVars {
instructions: "Be concise.".into(),
},
None, // variant = None → default arm
)?;
comp.append(
&user,
&UserVars {
query: "What is Rust?".into(),
},
None,
)?;
let messages = comp.resolve()?;
for m in &messages {
println!("{}: {}", m.role, m.text);
// "system: Be concise."
// "user: What is Rust?"
}
assert_eq!(
messages
.iter()
.map(|m| (m.role.as_str(), m.text.as_str()))
.collect::<Vec<_>>(),
vec![("system", "Be concise."), ("user", "What is Rust?")],
);
Ok(())
}

Composition::append<V>(&mut self, prompt: &Prompt, vars: &V, variant: Option<&str>) returns Result<(), ConsumerError>. On a garde failure the entry is NOT stored.

Composition::resolve(&self) returns Result<Vec<Message>, ConsumerError>. Each Message carries role: String and text: String. Composition::len() and is_empty() report the entry count.

Rust builds a composition with new() + append() only — there is no bulk from_messages constructor (that is the Python/TypeScript idiom); push each entry explicitly, as above.

{ role: string, text: string }

role is taken from the Prompt’s definition ("system" / "user" / "assistant"). text is the rendered body. Composition uses no guard expansion — the RenderResult.guard field is not accessible from composition; use individual Prompt.render calls to access the guard.

docs current as of 0.2.0