Skip to content
Prompting Press v0.2

Deriving a prompt

Prompt is immutable — there are no setters. derive is the single, general-purpose way to produce a changed prompt: it shallow-replaces any subset of top-level fields, re-validates the merged whole (agreement, parse, reserved-variant-name), and returns a new Prompt — the original is untouched. The method is spelled derive in all three languages.

It is general — use it to replace the body, rename, swap the variables map, adjust metadata, or add a variant at runtime. (To declare alternative bodies up front in the prompt document, that’s simpler — see Variants. This page is for deriving a changed copy of an already-constructed Prompt.)

Two complementary surfaces work together:

  • Read the current fields with the accessors.variants() (Rust) / .variants (Python & TypeScript properties), .body()/.name(), etc. These never mutate; they return what the prompt currently holds.
  • Derive a changed copy with the sole mutator derive. .variants() and derive are not alternatives — the accessor cannot change anything, and derive is the only thing that can; the “add a variant” example below uses both together (read with .variants(), write with derive).

Any top-level field of the prompt definition:

FieldOverlay type
namestring
role"system" | "user" | "assistant"
bodystring (template source)
variablesfull variables map (replaces the entire map)
variantsfull variants map (replaces the entire map)
output_modelstring | null (TS/Py) / Option<String> (Rust)
metadataopaque object

Fields absent from the overlay are kept from the original. Re-validation runs over the merged whole — so an overlay that introduces an agreement violation (a new body that references an undeclared variable) is rejected.

variants is replaced wholesale — read, then spread

Section titled “variants is replaced wholesale — read, then spread”

This is the one sharp edge: derive does a shallow replace per top-level field, so an overlay’s variants map replaces the entire existing map — it does not merge. When the prompt already has variants and the intent is to add one while keeping the rest, read the current map with .variants() and spread it into the overlay. (When the prompt has no variants yet, pass the new map directly — there is nothing to preserve.)

.variants() is a read accessor (it returns the current map; it never mutates). derive is the only mutator. The pattern below uses both together: read with .variants(), write with derive.

The examples on this page start from an assistant prompt (a company + max_words body) and a matching AssistantVars — the same pair from Getting started:

guides_derive_setup.rs
//! Derive guide — the starting pair: an `assistant` system prompt (a `company` +
//! `max_words` body) and a matching `AssistantVars`. Every later example on the page
//! derives from this. Standalone — `cargo run --example guides_derive_setup`.
use garde::Validate;
use prompting_press::Prompt;
use serde::Serialize;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
// The pair parses and validates: the body's {{ company }}/{{ max_words }} agree
// with AssistantVars.
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
assert_eq!(assistant.name(), "assistant");
// AssistantVars is a plain garde-validated struct — construct one to prove the shape.
let vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
assert_eq!(vars.company, "Acme Robotics");
assert_eq!(vars.max_words, 50);
Ok(())
}
guides_derive_add_variant.rs
//! Derive guide — add a variant at runtime: READ the current variants with the
//! `.variants()` accessor, add to a clone, then WRITE the merged map back via the sole
//! mutator `derive`. The original is untouched.
//! Standalone — `cargo run --example guides_derive_add_variant`.
use prompting_press::{Prompt, PromptOverlay};
use serde_json::json;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// READ the current variants, then add to a clone — so existing arms survive.
let mut variants = assistant.variants().clone();
variants.insert(
"formal".to_string(),
serde_json::from_value(json!({
"body": "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words."
}))?,
);
// WRITE the merged map back via the sole mutator.
let formal_assistant = assistant.derive(PromptOverlay {
variants: Some(variants),
..Default::default()
})?;
// assistant is unchanged; formal_assistant is a new, fully-validated Prompt.
assert!(assistant.variants().is_empty(), "original is untouched");
assert!(formal_assistant.variants().contains_key("formal"));
Ok(())
}

Replacing only the root body (the default arm):

guides_derive_replace_body.rs
//! Derive guide — replace only the root body (the default arm) with `derive`.
//! Standalone — `cargo run --example guides_derive_replace_body`.
use garde::Validate;
use prompting_press::{GuardConfig, Prompt, PromptOverlay};
use serde::Serialize;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let brief_assistant = assistant.derive(PromptOverlay {
body: Some("You are a support assistant for {{ company }}.".to_string()),
..Default::default()
})?;
let vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
let result = brief_assistant.render(&vars, None, &GuardConfig::default(), false)?;
assert_eq!(
result.text,
"You are a support assistant for Acme Robotics."
);
Ok(())
}

If the merged definition violates any construction invariant, derive returns an error (Rust Result::Err) or raises/throws. Example — overlaying a body that references an undeclared variable:

guides_derive_revalidation_error.rs
//! Derive guide — re-validation on overlay: overlaying a body that references an
//! undeclared variable is rejected over the merged whole (agreement failure).
//! Standalone — `cargo run --example guides_derive_revalidation_error`.
use prompting_press::{ConsumerError, Prompt, PromptOverlay};
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let bad = assistant.derive(PromptOverlay {
body: Some("You help {{ ghost }}.".to_string()),
..Default::default()
});
match bad {
Err(ConsumerError::Kernel(rows)) => {
assert_eq!(rows[0].code, "undefined_variable");
assert_eq!(rows[0].field, "ghost");
}
_ => unreachable!("the merged definition is agreement-unsound"),
}
Ok(())
}

Validators carry forward (Python / TypeScript)

Section titled “Validators carry forward (Python / TypeScript)”

In Python and TypeScript, the validators supplied at construction carry forward to the derived Prompt by default. Pass validators=NewModel (Python) or derive(overlay, newSchema) (TypeScript) to override.

After adding a variant, select it by name at render time:

guides_derive_render_variant.rs
//! Derive guide — render a named variant: after adding a variant with `derive`, select
//! it by name at render time. Variant selection is caller-owned.
//! Standalone — `cargo run --example guides_derive_render_variant`.
use garde::Validate;
use prompting_press::{GuardConfig, Prompt, PromptOverlay};
use serde::Serialize;
use serde_json::json;
use std::fs;
#[derive(Serialize, Validate)]
struct AssistantVars {
#[garde(length(min = 1))]
company: String,
#[garde(range(min = 1))]
max_words: i64,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let dir = concat!(env!("CARGO_MANIFEST_DIR"), "/examples");
let assistant = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
let mut variants = assistant.variants().clone();
variants.insert(
"formal".to_string(),
serde_json::from_value(json!({
"body": "You are the official support assistant for {{ company }}. Please keep every reply under {{ max_words }} words."
}))?,
);
let formal_assistant = assistant.derive(PromptOverlay {
variants: Some(variants),
..Default::default()
})?;
let vars = AssistantVars {
company: "Acme Robotics".into(),
max_words: 50,
};
let result = formal_assistant.render(&vars, Some("formal"), &GuardConfig::default(), false)?;
assert_eq!(
result.text,
"You are the official support assistant for Acme Robotics. Please keep every reply under 50 words."
);
assert_eq!(result.variant, "formal");
Ok(())
}

Variant selection is caller-owned — the library validates the name and renders it. It does not own experiment-assignment logic or choose variants automatically.

docs current as of 0.2.0