use std::{path::Path, sync::Arc};
use mentra::ModelInfo;
use serde_json::{Value, json};
use crate::workspace::ToolRoster;
#[derive(Debug)]
pub struct ChildContext<'a> {
prompt: &'a str,
parent_agent_id: &'a str,
workspace_dir: &'a Path,
}
impl<'a> ChildContext<'a> {
pub(crate) fn new(prompt: &'a str, parent_agent_id: &'a str, workspace_dir: &'a Path) -> Self {
Self {
prompt,
parent_agent_id,
workspace_dir,
}
}
pub fn prompt(&self) -> &str {
self.prompt
}
pub fn parent_agent_id(&self) -> &str {
self.parent_agent_id
}
pub fn workspace_dir(&self) -> &Path {
self.workspace_dir
}
}
#[derive(Debug, Clone, Default)]
pub struct ChildSpec {
pub(crate) roster: Option<ToolRoster>,
pub(crate) model: Option<ModelInfo>,
pub(crate) system: Option<String>,
}
impl ChildSpec {
pub fn inherit() -> Self {
Self::default()
}
#[must_use]
pub fn with_roster(self, roster: ToolRoster) -> Self {
Self {
roster: Some(roster),
..self
}
}
#[must_use]
pub fn with_model(self, model: ModelInfo) -> Self {
Self {
model: Some(model),
..self
}
}
#[must_use]
pub fn with_system(self, system: impl Into<String>) -> Self {
Self {
system: Some(system.into()),
..self
}
}
pub(crate) fn is_inherit(&self) -> bool {
self.roster.is_none() && self.model.is_none() && self.system.is_none()
}
pub(crate) fn overrides_roster(&self) -> bool {
self.roster.is_some()
}
pub(crate) fn preview_value(&self) -> Option<Value> {
if self.is_inherit() {
return None;
}
let mut child = serde_json::Map::new();
if let Some(model) = &self.model {
child.insert(
"model".to_string(),
json!({ "id": model.id, "provider": model.provider.as_str() }),
);
}
if let Some(roster) = &self.roster {
let profile = roster.as_profile();
let described = match &profile.allowed_tools {
Some(offered) => json!({ "offered": offered }),
None => json!({ "hidden": profile.hidden_tools }),
};
child.insert("roster".to_string(), described);
}
if self.system.is_some() {
child.insert("system".to_string(), json!("replaced"));
}
Some(Value::Object(child))
}
}
pub(crate) type ChildPolicy = Arc<dyn Fn(&ChildContext<'_>) -> ChildSpec + Send + Sync>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inherit_says_nothing_to_the_approver() {
assert_eq!(ChildSpec::inherit().preview_value(), None);
assert!(ChildSpec::inherit().is_inherit());
}
#[test]
fn every_override_is_described_and_the_system_text_is_not() {
let spec = ChildSpec::inherit()
.with_roster(ToolRoster::only(["read", "grep"]))
.with_model(ModelInfo::new("cheap-model", "openai"))
.with_system("secret internal triage instructions");
let child = spec.preview_value().expect("overrides are described");
assert_eq!(
child["model"],
json!({ "id": "cheap-model", "provider": "openai" }),
"an id alone would hide a vendor switch from the operator"
);
assert_eq!(child["roster"], json!({ "offered": ["grep", "read"] }));
assert_eq!(
child["system"], "replaced",
"the fact travels; the text does not"
);
assert!(
!child.to_string().contains("secret"),
"a preview travels further than a glance: {child}"
);
}
#[test]
fn a_hide_roster_is_described_as_what_it_hides() {
let spec = ChildSpec::inherit().with_roster(ToolRoster::hide(["write"]));
let child = spec.preview_value().expect("an override is described");
let hidden = child["roster"]["hidden"]
.as_array()
.expect("a denylist roster lists what it hides");
assert!(hidden.iter().any(|name| name == "write"), "{child}");
}
}