Skip to content
Prompting Press v0.5

Deriving a prompt

Prompt is immutable — there are no setters. derive is the single, general-purpose way to produce a changed prompt: it combines an overlay with the current definition using a merge strategy, 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, add a variant at runtime, or union variables from a base into a child. (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.

derive supports two strategies, selected per-call (default is Replace):

StrategyMap fields (variables, variants, metadata)Scalar fields (name, role, body, output_model)
Replace (default)Overlay’s map replaces the base’s map wholesaleOverlay’s value replaces when present
MergeTop-level keys union (child-wins on collision, whole entry — no recursion)Overlay’s value replaces when present

Replace is the pre-0.3 behavior, kept as the default — no existing call site changes.

Merge is the new strategy. Use it when you want a child prompt that inherits the base’s declared variables and adds its own — without hand-spreading the base’s variables into every overlay. In TypeScript and Python the strategy is derive’s options argument ({ strategy } / strategy=); in Rust it rides in DeriveOptions on derive_with:

guides_derive_merge.rs
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! Derive guide — `MergeStrategy::Merge` via `derive_with`: union the base's declared
//! variables with the overlay's, so a child prompt inherits `company` + `max_words` and adds
//! its own `tone` without hand-spreading the base's variables. The base is untouched.
//! Standalone — `cargo run --example guides_derive_merge`.
use prompting_press::{DeriveOptions, MergeStrategy, 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 base = Prompt::from_yaml(&fs::read_to_string(format!("{dir}/assistant.yaml"))?)?;
// Merge unions the map-typed fields (variables/variants/metadata) at their top-level
// keys — child-wins on collision. The base's `company` + `max_words` survive; the
// overlay only declares what it adds. `derive_with` takes the strategy in DeriveOptions.
let child = base.derive_with(
PromptOverlay {
body: Some(
"You are a {{ tone }} assistant for {{ company }}. \
Keep replies under {{ max_words }} words."
.to_string(),
),
variables: Some(serde_json::from_value(json!({
"tone": { "type": "string", "trusted": true }
}))?),
..Default::default()
},
DeriveOptions {
strategy: MergeStrategy::Merge,
},
)?;
// child inherited the base's two variables and gained its own — three in total.
assert!(child.variables().contains_key("company"));
assert!(child.variables().contains_key("max_words"));
assert!(child.variables().contains_key("tone"));
assert_eq!(child.variables().len(), 3);
// base is untouched: no `tone` leaked back onto it.
assert!(!base.variables().contains_key("tone"));
Ok(())
}

The agreement check is name-only (referenced variables ⊆ declared variables). This means:

  • A Merge that effectively removes a variable a base body or variant still references fails construction (agreement check catches it).
  • A Merge that replaces a variable’s declaration (e.g. changes type or trusted) is accepted — that is the validator’s responsibility, not the kernel’s.

Only Replace and Merge are supported. There is no recursive / deep merge and no no-op mode. These axes are reserved for a future consumer that earns them; they are not anticipated.

metadata (including guard keys) under Merge

Section titled “metadata (including guard keys) under Merge”

Metadata keys union at top-level under Merge. If the overlay supplies a guard key, the overlay’s whole guard entry replaces the base’s (child-wins-whole-entry). The library does not interpret metadata contents — the union is opaque.

variants is replaced wholesale under Replace — read, then spread (or use Merge)

Section titled “variants is replaced wholesale under Replace — read, then spread (or use Merge)”

Under the default Replace strategy, an overlay’s variants map replaces the entire existing map. When the prompt already has variants and the intent is to add one while keeping the rest, either:

  • Use MergeStrategy.Merge / MergeStrategy::Merge — the union is automatic, and
  • Read the current map with .variants() and spread it into the overlay — the manual pre-0.3 idiom, still valid under Replace.

.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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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, { validators: newSchema }) (TypeScript) to override. Coverage is re-checked against the merged variable set — not the base’s.

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

guides_derive_render_variant.rs
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! 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.5.0