use super::*;
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 percent = match region_value.get("budget").and_then(|v| v.as_str()) {
Some(s) => Some(crate::BudgetSpec::parse_budget(s).map_err(Error::Other)?),
None => None,
};
let max_tokens_opt = region_value
.get("max_tokens")
.and_then(|v| v.as_integer())
.map(|v| v as usize);
let min_tokens = region_value
.get("min_tokens")
.and_then(|v| v.as_integer())
.map(|v| v as usize);
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 region_value.get("compact_at").and_then(|v| v.as_str()) {
Some(s) => Some(crate::BudgetSpec::parse_budget(s).map_err(Error::Other)?),
None => None,
};
let explicit_threshold = region_value
.get("threshold_tokens")
.and_then(|v| v.as_integer())
.map(|v| v as usize);
let kind_str = region_value
.get("kind")
.and_then(|v| v.as_str())
.unwrap_or("temporary");
let kind = match kind_str {
"pinned" => RegionKind::Pinned,
"sliding_window" => {
let max_items = region_value
.get("max_items")
.and_then(|v| v.as_integer())
.unwrap_or(10) as usize;
let eviction_strategy = match region_value.get("strategy").and_then(|v| v.as_str())
{
Some("bulk") => {
let overflow = region_value
.get("overflow")
.and_then(|v| v.as_integer())
.unwrap_or(10) as usize;
EvictionStrategy::Bulk { overflow }
}
Some("compact") => {
let compact_count = region_value
.get("compact_count")
.and_then(|v| v.as_integer())
.unwrap_or(10) as usize;
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 * 8 / 10,
};
RegionKind::Compacting {
threshold_tokens: threshold,
}
}
"clearable" => RegionKind::Clearable,
"compact_history" => {
let source = region_value
.get("source_region")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
RegionKind::CompactHistory {
source_region: source,
}
}
"checklist" => RegionKind::Checklist,
"hashmap" | "hash_map" => {
let max_entries = region_value
.get("max_entries")
.and_then(|v| v.as_integer())
.map(|v| v as usize);
RegionKind::HashMap { max_entries }
}
"custom" => {
let script = region_value
.get("script")
.and_then(|v| v.as_str())
.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 = region_value
.get("persistent")
.and_then(|v| v.as_bool())
.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 = region_value
.get("required")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let required_message = region_value
.get("required_message")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let summarizable = region_value
.get("summarizable")
.and_then(|v| v.as_bool())
.unwrap_or(true);
let seed = parse_region_seed(region_name, region_value.get("seed"));
if percent.is_none() {
total_tokens += 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;
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 v.get("transform").and_then(|x| x.as_str()) {
Some("direct") => Some(ContentTransform::Direct),
Some("summarize") => Some(ContentTransform::Summarize),
Some("extract") => Some(ContentTransform::Extract {
fields: v
.get("fields")
.and_then(|x| x.as_array())
.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) = t.get("glob").and_then(|v| v.as_str()) {
Some(RegionSeed::Glob {
pattern: pattern.to_string(),
})
} else if let Some(files) = t.get("files").and_then(|v| v.as_array()) {
Some(RegionSeed::Files {
paths: files
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect(),
})
} else if let Some(text) = t.get("literal").and_then(|v| v.as_str()) {
Some(RegionSeed::Literal {
text: text.to_string(),
})
} else if let Some(script) = t.get("rhai").and_then(|v| v.as_str()) {
Some(RegionSeed::Rhai {
script: script.to_string(),
})
} else if let Some(command) = t.get("command").and_then(|v| v.as_str()) {
Some(RegionSeed::Command {
command: command.to_string(),
})
} else {
t.get("caller")
.and_then(|v| v.as_str())
.map(|name| RegionSeed::CallerInput {
name: name.to_string(),
})
}
}
_ => None,
}
}