use super::*;
use crate::layout::SeedToolCall;
pub(super) fn parse_region_layout(
regions_table: &toml::value::Table,
) -> Result<(Vec<RegionDefinition>, usize)> {
let mut regions = Vec::new();
let mut total_tokens = 0usize;
for (region_name, region_value) in regions_table {
let where_ = format!("region '{region_name}'");
let count = |key: &str| count_of(region_value, &where_, key);
let percent = match str_of(region_value, "budget") {
Some(s) => Some(crate::BudgetSpec::parse_budget(s).map_err(Error::Other)?),
None => None,
};
let max_tokens_opt = count("max_tokens")?;
let min_tokens = count("min_tokens")?;
let budget = match percent {
Some(percent) => crate::BudgetSpec::Percent {
percent,
min: min_tokens,
max: max_tokens_opt,
},
None => crate::BudgetSpec::Absolute(max_tokens_opt.unwrap_or(5000)),
};
let provisional_max_tokens = match &budget {
crate::BudgetSpec::Absolute(n) => *n,
crate::BudgetSpec::Percent { max, .. } => max.unwrap_or(0),
};
let compact_at = match str_of(region_value, "compact_at") {
Some(s) => Some(crate::BudgetSpec::parse_budget(s).map_err(Error::Other)?),
None => None,
};
let explicit_threshold = count("threshold_tokens")?;
let kind_str = str_of(region_value, "kind").unwrap_or("temporary");
let kind = match kind_str {
"pinned" => RegionKind::Pinned,
"sliding_window" => {
let max_items = count("max_items")?.unwrap_or(10);
let eviction_strategy = match str_of(region_value, "strategy") {
Some("bulk") => {
let overflow = count("overflow")?.unwrap_or(10);
EvictionStrategy::Bulk { overflow }
}
Some("compact") => {
let compact_count = count("compact_count")?.unwrap_or(10);
EvictionStrategy::Compact { compact_count }
}
Some("per_item") | None => EvictionStrategy::PerItem,
Some(other) => {
return Err(Error::Other(format!(
"region '{region_name}': strategy \"{other}\" is not \
valid (valid: per_item, bulk, compact)"
)));
}
};
RegionKind::SlidingWindow {
max_items,
eviction_strategy,
}
}
"temporary" => RegionKind::Temporary,
"compacting" => {
let threshold = match (compact_at, explicit_threshold, percent.is_some()) {
(Some(_), Some(cap), _) => cap,
(Some(_), None, _) => usize::MAX,
(None, Some(t), _) => t,
(None, None, true) => usize::MAX,
(None, None, false) => provisional_max_tokens.saturating_mul(8) / 10,
};
RegionKind::Compacting {
threshold_tokens: threshold,
}
}
"clearable" => RegionKind::Clearable,
"compact_history" => {
let source = str_of(region_value, "source_region")
.unwrap_or("")
.to_string();
RegionKind::CompactHistory {
source_region: source,
}
}
"checklist" => RegionKind::Checklist,
"hashmap" | "hash_map" => {
let max_entries = count("max_entries")?;
RegionKind::HashMap { max_entries }
}
"custom" => {
let script = str_of(region_value, "script")
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| {
Error::Other(format!(
"region '{region_name}': kind = \"custom\" requires \
script = \"<path>.rhai\""
))
})?
.to_string();
let persistent = bool_of(region_value, "persistent").unwrap_or(false);
RegionKind::Custom { script, persistent }
}
unknown => {
return Err(Error::Other(format!(
"region '{region_name}': unknown kind \"{unknown}\" (valid kinds: \
pinned, sliding_window, temporary, compacting, clearable, \
compact_history, checklist, hashmap, custom)"
)));
}
};
let compact_at_field = match (kind_str, compact_at, explicit_threshold, percent.is_some()) {
("compacting", Some(f), _, _) => Some(f),
("compacting", None, None, true) => Some(0.80),
_ => None,
};
let required = bool_of(region_value, "required").unwrap_or(false);
let required_message = str_of(region_value, "required_message").map(|s| s.to_string());
let summarizable = bool_of(region_value, "summarizable").unwrap_or(true);
let admission = match str_of(region_value, "admission") {
Some("reject") => crate::region::Admission::Reject,
Some("evict") | None => crate::region::Admission::Evict,
Some(other) => {
return Err(crate::error::Error::ValidationFailed(format!(
"region '{region_name}' has admission = \"{other}\"; \
expected \"evict\" or \"reject\""
)));
}
};
let volatility = match str_of(region_value, "volatility") {
Some("stable") => crate::region::Volatility::Stable,
Some("grows") => crate::region::Volatility::Grows,
Some("rewritten") | None => crate::region::Volatility::Rewritten,
Some(other) => {
return Err(crate::error::Error::ValidationFailed(format!(
"region '{region_name}' has volatility = \"{other}\"; \
expected \"stable\", \"grows\" or \"rewritten\""
)));
}
};
let description = str_of(region_value, "description")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let describe_in_prompt = bool_of(region_value, "describe_in_prompt").unwrap_or(false);
let seed = parse_region_seed(region_name, region_value.get("seed"));
if percent.is_none() {
total_tokens = total_tokens.saturating_add(provisional_max_tokens);
}
let mut def = RegionDefinition::new(region_name.clone(), kind, provisional_max_tokens)
.with_budget(budget)
.with_required(required, required_message);
def.summarizable = summarizable;
def.admission = admission;
def.description = description;
def.describe_in_prompt = describe_in_prompt;
def.volatility = volatility;
if let Some(f) = compact_at_field {
def = def.with_compact_at(f);
}
if let Some(seed) = seed {
def = def.with_seed(seed);
}
regions.push(def);
}
Ok((regions, total_tokens))
}
pub(super) fn parse_region_mapping(v: &toml::Value) -> RegionMapping {
let transform = match str_of(v, "transform") {
Some("direct") => Some(ContentTransform::Direct),
Some("summarize") => Some(ContentTransform::Summarize),
Some("extract") => Some(ContentTransform::Extract {
fields: array_of(v, "fields")
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
}),
_ => None,
};
RegionMapping {
from_region: str_field(v, "from_region"),
to_region: str_field(v, "to_region"),
transform,
}
}
pub(super) fn parse_region_seed(
region_name: &str,
value: Option<&toml::Value>,
) -> Option<RegionSeed> {
let Some(value) = value else {
return (region_name == "task").then(|| RegionSeed::CallerInput {
name: "task".to_string(),
});
};
match value {
toml::Value::String(s) => Some(match s.as_str() {
"task_input" => RegionSeed::CallerInput {
name: "task".to_string(),
},
"input" => RegionSeed::CallerInput {
name: region_name.to_string(),
},
other => RegionSeed::CallerInput {
name: other.to_string(),
},
}),
toml::Value::Table(t) => {
if let Some(pattern) = str_of(t, "glob") {
Some(RegionSeed::Glob {
pattern: pattern.to_string(),
})
} else if let Some(files) = array_of(t, "files") {
Some(RegionSeed::Files {
paths: files
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
})
} else if let Some(text) = str_of(t, "literal") {
Some(RegionSeed::Literal {
text: text.to_string(),
})
} else if let Some(script) = str_of(t, "rhai") {
Some(RegionSeed::Rhai {
script: script.to_string(),
})
} else if let Some(command) = str_of(t, "command") {
Some(RegionSeed::Command {
command: command.to_string(),
})
} else if let Some(name) = str_of(t, "tool") {
Some(RegionSeed::Tools {
calls: vec![SeedToolCall::new(name)],
refresh: parse_seed_refresh(t),
})
} else if let Some(list) = array_of(t, "tools") {
let calls: Vec<SeedToolCall> =
list.iter().filter_map(parse_seed_tool_call).collect();
(!calls.is_empty()).then_some(RegionSeed::Tools {
calls,
refresh: parse_seed_refresh(t),
})
} else {
str_of(t, "caller").map(|name| RegionSeed::CallerInput {
name: name.to_string(),
})
}
}
_ => None,
}
}
fn parse_seed_refresh(table: &toml::value::Table) -> crate::layout::SeedRefresh {
str_of(table, "refresh")
.and_then(crate::layout::SeedRefresh::from_str_loose)
.unwrap_or_default()
}
fn parse_seed_tool_call(value: &toml::Value) -> Option<SeedToolCall> {
match value {
toml::Value::String(name) => Some(SeedToolCall::new(name.as_str())),
toml::Value::Table(t) => {
let name = str_of(t, "name")?;
match t.get("args") {
Some(args) => Some(SeedToolCall::with_args(
name,
serde_json::to_value(args).expect("a parsed TOML value converts to JSON"),
)),
None => Some(SeedToolCall::new(name)),
}
}
_ => None,
}
}
#[cfg(test)]
pub(super) const REGION_KEYS: &[&str] = &[
"admission",
"budget",
"compact_at",
"compact_count",
"describe_in_prompt",
"description",
"kind",
"max_entries",
"max_items",
"max_tokens",
"min_tokens",
"overflow",
"persistent",
"required",
"required_message",
"script",
"seed",
"source_region",
"strategy",
"summarizable",
"threshold_tokens",
"volatility",
];