use super::reminders::*;
use super::*;
#[derive(Clone, Copy)]
pub(super) enum SystemPromptPosition {
Before,
After,
}
pub(super) fn system_prompt_error(message: impl Into<String>) -> VmError {
VmError::Thrown(VmValue::String(arcstr::ArcStr::from(message.into())))
}
pub(super) fn system_fragment_position(
value: Option<&VmValue>,
source: &str,
) -> Result<SystemPromptPosition, VmError> {
let Some(value) = value else {
return Ok(SystemPromptPosition::Before);
};
match value {
VmValue::Nil => Ok(SystemPromptPosition::Before),
VmValue::String(raw) => match raw.as_ref() {
"before" => Ok(SystemPromptPosition::Before),
"after" => Ok(SystemPromptPosition::After),
other => Err(system_prompt_error(format!(
"{source}.position: expected \"before\" or \"after\", got \"{other}\""
))),
},
other => Err(system_prompt_error(format!(
"{source}.position: expected a string, got {}",
other.type_name()
))),
}
}
pub(super) fn enabled_system_prompt_part(part: &crate::value::DictMap) -> bool {
!matches!(
part.get("enabled"),
Some(VmValue::Bool(false) | VmValue::Nil)
)
}
const SYSTEM_FRAGMENT_KEYS: &[&str] = &["content", "title", "position", "enabled"];
const SYSTEM_FRAGMENT_SHAPE: &str =
"a system fragment is a string, or {content: string, title?: string, \
position?: \"before\"|\"after\", enabled?: bool}";
fn system_fragment_alias_fix(key: &str) -> Option<&'static str> {
match key {
"text" | "prompt" => Some("content"),
"label" | "name" => Some("title"),
_ => None,
}
}
fn validate_system_fragment_keys(
part: &crate::value::DictMap,
source: &str,
) -> Result<(), VmError> {
for (key, _) in part.iter() {
let key: &str = key.as_ref();
if SYSTEM_FRAGMENT_KEYS.contains(&key) {
continue;
}
return Err(system_prompt_error(match system_fragment_alias_fix(key) {
Some(canonical) => format!(
"{source}: fragment key `{key}` was removed — use `{canonical}`. {SYSTEM_FRAGMENT_SHAPE}."
),
None => format!("{source}: unknown fragment key `{key}` — {SYSTEM_FRAGMENT_SHAPE}."),
}));
}
Ok(())
}
pub(super) fn render_system_fragment(content: &str, part: &crate::value::DictMap) -> String {
let title = part.get("title").map(VmValue::display).unwrap_or_default();
let title = title.trim();
let content = content.trim();
if title.is_empty() {
content.to_string()
} else {
format!("## {title}\n{content}")
}
}
pub(super) fn append_system_list_fragments(
out: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&crate::value::DictMap>,
) -> Result<(), VmError> {
let Some(VmValue::List(items)) = options.and_then(|options| options.get("system")) else {
return Ok(());
};
for (index, item) in items.iter().enumerate() {
append_system_fragment(out, item, index)?;
}
Ok(())
}
fn append_system_fragment(
out: &mut Vec<crate::llm::prompt::PromptFragment>,
item: &VmValue,
index: usize,
) -> Result<(), VmError> {
use crate::llm::prompt::PromptFragment;
let source = format!("system[{index}]");
match item {
VmValue::String(text) => {
out.push(PromptFragment::new(
source.clone(),
source,
fragment_bucket(SystemPromptPosition::Before),
text.to_string(),
));
Ok(())
}
VmValue::Dict(part) => {
validate_system_fragment_keys(part, &source)?;
if !enabled_system_prompt_part(part) {
return Ok(());
}
let content = part.get("content").map(VmValue::display).ok_or_else(|| {
system_prompt_error(format!(
"{source}: fragment dict must include `content` — {SYSTEM_FRAGMENT_SHAPE}."
))
})?;
let position = system_fragment_position(part.get("position"), &source)?;
out.push(PromptFragment::new(
source.clone(),
source,
fragment_bucket(position),
render_system_fragment(&content, part),
));
Ok(())
}
other => Err(system_prompt_error(format!(
"{source}: {SYSTEM_FRAGMENT_SHAPE}; got {}",
other.type_name()
))),
}
}
pub(super) fn fragment_bucket(
position: SystemPromptPosition,
) -> crate::llm::prompt::FragmentBucket {
match position {
SystemPromptPosition::Before => crate::llm::prompt::FragmentBucket::Before,
SystemPromptPosition::After => crate::llm::prompt::FragmentBucket::After,
}
}
pub(super) fn system_prompt_fingerprint(system: &str) -> String {
use sha2::Digest as _;
let digest = sha2::Sha256::digest(system.as_bytes());
format!("sha256:{}", hex::encode(digest))
}
pub(crate) fn system_prompt_metadata(system: &str) -> serde_json::Value {
let fingerprint = system_prompt_fingerprint(system);
serde_json::json!({
"content": system,
"hash": fingerprint,
"sha256": fingerprint,
"bytes": system.len(),
})
}
pub(crate) fn system_prompt_event_metadata(system: &str) -> serde_json::Value {
let fingerprint = system_prompt_fingerprint(system);
serde_json::json!({
"hash": fingerprint,
"sha256": fingerprint,
"bytes": system.len(),
})
}
pub(crate) fn compose_system_prompt(
system: Option<String>,
options: Option<&crate::value::DictMap>,
) -> Result<Option<String>, VmError> {
compose_system_prompt_with_reminders(system, options, &[])
}
pub(super) fn compose_system_prompt_with_reminders(
system: Option<String>,
options: Option<&crate::value::DictMap>,
rendered_reminders: &[RenderedReminder],
) -> Result<Option<String>, VmError> {
Ok(assemble_system_prompt(system, options, rendered_reminders)?.system)
}
pub(crate) fn assemble_system_prompt(
system: Option<String>,
options: Option<&crate::value::DictMap>,
rendered_reminders: &[RenderedReminder],
) -> Result<crate::llm::prompt::AssembledPrompt, VmError> {
use crate::llm::prompt::{assemble, FragmentBucket, PromptFragment};
let mut fragments: Vec<PromptFragment> = Vec::new();
append_system_list_fragments(&mut fragments, options)?;
let decomposed = append_decomposed_primary_fragments(&mut fragments, options)?;
if !decomposed {
let primary_system = system
.filter(|system| !system.trim().is_empty())
.or_else(|| {
options
.and_then(|options| options.get("system"))
.and_then(|value| match value {
VmValue::String(text) => Some(text.to_string()),
_ => None,
})
.filter(|system| !system.trim().is_empty())
});
if let Some(system) = primary_system {
fragments.push(PromptFragment::new(
"primary",
"primary",
FragmentBucket::Before,
system,
));
}
append_context_profile_fragments(&mut fragments, options);
}
append_tool_guidance_fragments(&mut fragments, options);
let _ = rendered_reminders;
let ctx = assemble_ctx(options);
Ok(assemble(&fragments, &ctx))
}
pub(super) fn tool_names_from_options(
options: Option<&crate::value::DictMap>,
) -> std::collections::BTreeSet<String> {
let mut names = std::collections::BTreeSet::new();
let Some(list) = options.and_then(|options| tool_entry_list(options.get("tools"))) else {
return names;
};
for entry in list.iter() {
if let Some(name) = entry
.as_dict()
.and_then(|dict| dict.get("name"))
.map(VmValue::display)
.filter(|name| !name.is_empty())
{
names.insert(name);
}
}
names
}
pub(super) fn tool_entry_list(value: Option<&VmValue>) -> Option<Vec<VmValue>> {
match value? {
VmValue::List(items) => Some((**items).clone()),
VmValue::Dict(dict) => match dict.get("tools") {
Some(VmValue::List(items)) => Some((**items).clone()),
_ => None,
},
_ => None,
}
}
pub(super) fn append_tool_guidance_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&crate::value::DictMap>,
) {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(list) = options.and_then(|options| tool_entry_list(options.get("tools"))) else {
return;
};
for entry in list.iter() {
let Some(dict) = entry.as_dict() else {
continue;
};
let Some(name) = dict
.get("name")
.map(VmValue::display)
.filter(|name| !name.is_empty())
else {
continue;
};
let guidance = dict
.get("guidance")
.map(VmValue::display)
.map(|text| text.trim().to_string())
.filter(|text| !text.is_empty());
let Some(guidance) = guidance else {
continue;
};
fragments.push(
PromptFragment::new(
format!("tool:{name}.guidance"),
format!("tool:{name}"),
FragmentBucket::Before,
guidance,
)
.requiring_tools(vec![name]),
);
}
}
pub(super) fn append_decomposed_primary_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&crate::value::DictMap>,
) -> Result<bool, VmError> {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(VmValue::List(items)) = options.and_then(|options| options.get("_system_fragments"))
else {
return Ok(false);
};
for (index, item) in items.iter().enumerate() {
let Some(dict) = item.as_dict() else {
continue;
};
let Some(body) = dict.get("body").map(VmValue::display) else {
continue;
};
let id = dict
.get("id")
.map(VmValue::display)
.filter(|id| !id.is_empty())
.unwrap_or_else(|| format!("primary[{index}]"));
let source = dict
.get("source")
.map(VmValue::display)
.filter(|source| !source.is_empty())
.unwrap_or_else(|| "primary".to_string());
let requires_tools = match dict.get("requires_tools") {
Some(VmValue::List(tools)) => tools.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
let bucket = match dict
.get("bucket")
.map(VmValue::display)
.map(|bucket| bucket.trim().to_ascii_lowercase())
.as_deref()
{
None | Some("") | Some("before") => FragmentBucket::Before,
Some("after") => FragmentBucket::After,
Some(other) => {
return Err(VmError::Runtime(format!(
"_system_fragments[{index}].bucket must be \"before\" or \"after\"; got {other:?}"
)));
}
};
let requires_caps = match dict.get("requires_caps") {
Some(VmValue::List(caps)) => caps.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
fragments.push(
PromptFragment::new(id, source, bucket, body)
.requiring_tools(requires_tools)
.requiring_caps(requires_caps),
);
}
Ok(true)
}
pub(super) fn append_context_profile_fragments(
fragments: &mut Vec<crate::llm::prompt::PromptFragment>,
options: Option<&crate::value::DictMap>,
) {
use crate::llm::prompt::{FragmentBucket, PromptFragment};
let Some(profile) = options
.and_then(|options| options.get("context_profile"))
.and_then(VmValue::as_dict)
else {
return;
};
let Some(VmValue::List(items)) = profile.get("prompt_fragments") else {
return;
};
for (index, item) in items.iter().enumerate() {
let Some(dict) = item.as_dict() else {
continue;
};
let Some(body) = dict
.get("body")
.or_else(|| dict.get("content"))
.map(VmValue::display)
.map(|body| body.trim().to_string())
.filter(|body| !body.is_empty())
else {
continue;
};
let id = dict
.get("id")
.map(VmValue::display)
.filter(|id| !id.is_empty())
.unwrap_or_else(|| format!("profile[{index}]"));
let source = dict
.get("source")
.map(VmValue::display)
.filter(|source| !source.is_empty())
.unwrap_or_else(|| "profile".to_string());
let requires_tools = match dict.get("requires_tools") {
Some(VmValue::List(tools)) => tools.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
let requires_caps = match dict.get("requires_caps") {
Some(VmValue::List(caps)) => caps.iter().map(VmValue::display).collect(),
_ => Vec::new(),
};
fragments.push(
PromptFragment::new(id, source, FragmentBucket::Before, body)
.requiring_tools(requires_tools)
.requiring_caps(requires_caps),
);
}
}
pub(super) fn assemble_ctx(
options: Option<&crate::value::DictMap>,
) -> crate::llm::prompt::AssembleCtx {
crate::llm::prompt::AssembleCtx {
tool_names: tool_names_from_options(options),
caps: caps_from_options(options),
}
}
pub(super) fn caps_from_options(
options: Option<&crate::value::DictMap>,
) -> std::collections::BTreeSet<String> {
let mut caps = std::collections::BTreeSet::new();
let Some(options) = options else {
return caps;
};
collect_caps(options.get("capabilities"), &mut caps);
if let Some(profile) = options.get("context_profile").and_then(VmValue::as_dict) {
collect_caps(profile.get("caps"), &mut caps);
}
caps
}
pub(super) fn collect_caps(value: Option<&VmValue>, out: &mut std::collections::BTreeSet<String>) {
match value {
Some(VmValue::List(items)) => {
for item in items.iter() {
let cap = item.display();
if !cap.is_empty() {
out.insert(cap);
}
}
}
Some(VmValue::Dict(dict)) => {
for (key, value) in dict.iter() {
if !matches!(value, VmValue::Bool(false) | VmValue::Nil) {
out.insert(key.to_string());
}
}
}
Some(value) => {
let cap = value.display();
if !cap.is_empty() {
out.insert(cap);
}
}
None => {}
}
}