use mentra::{ModelInfo, ProviderRequestOptions, ToolResultPagingConfig};
use crate::{compaction::Compaction, context::SystemPrompt};
use super::ToolRoster;
#[derive(Clone, Default, PartialEq, Eq)]
pub struct RunProfile {
resolved_model: Option<ModelInfo>,
tool_roster: Option<ToolRoster>,
provider_request_options: Option<ProviderRequestOptions>,
max_output_tokens: Option<Option<u32>>,
compaction: Option<Compaction>,
tool_result_paging: Option<Option<ToolResultPagingConfig>>,
system_prompt: Option<SystemPrompt>,
}
impl std::fmt::Debug for RunProfile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RunProfile")
.field(
"resolved_model",
&self
.resolved_model
.as_ref()
.map(|model| (model.id.as_str(), model.provider.as_str())),
)
.field("tool_roster", &set_if(self.tool_roster.as_ref()))
.field(
"provider_request_options",
&redacted_if(self.provider_request_options.as_ref()),
)
.field("max_output_tokens", &set_or_clear(&self.max_output_tokens))
.field("compaction", &self.compaction)
.field(
"tool_result_paging",
&set_or_clear(&self.tool_result_paging),
)
.field(
"system_prompt",
&self.system_prompt.as_ref().map(|prompt| match prompt {
SystemPrompt::Replace(_) => "<replace redacted>",
SystemPrompt::Append(_) => "<append redacted>",
}),
)
.finish()
}
}
impl RunProfile {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_resolved_model(self, model: ModelInfo) -> Self {
Self {
resolved_model: Some(model),
..self
}
}
#[must_use]
pub fn with_tool_roster(self, tool_roster: ToolRoster) -> Self {
Self {
tool_roster: Some(tool_roster),
..self
}
}
#[must_use]
pub fn with_provider_request_options(self, options: ProviderRequestOptions) -> Self {
Self {
provider_request_options: Some(options),
..self
}
}
#[must_use]
pub fn with_max_output_tokens(self, max_output_tokens: Option<u32>) -> Self {
Self {
max_output_tokens: Some(max_output_tokens),
..self
}
}
#[must_use]
pub fn with_compaction(self, compaction: Compaction) -> Self {
Self {
compaction: Some(compaction),
..self
}
}
#[must_use]
pub fn with_tool_result_paging(
self,
tool_result_paging: Option<ToolResultPagingConfig>,
) -> Self {
Self {
tool_result_paging: Some(tool_result_paging),
..self
}
}
#[must_use]
pub fn with_system_prompt(self, system_prompt: SystemPrompt) -> Self {
Self {
system_prompt: Some(system_prompt),
..self
}
}
pub(crate) fn resolved_model(&self) -> Option<&ModelInfo> {
self.resolved_model.as_ref()
}
pub(crate) fn decides_reasoning(&self) -> bool {
self.provider_request_options.is_some()
}
pub(crate) fn has_extra_headers(&self) -> bool {
self.provider_request_options
.as_ref()
.is_some_and(|options| !options.session.extra_headers.is_empty())
}
pub(crate) fn unsupported_on_resume(&self) -> Option<&'static str> {
if self.tool_roster.is_some() {
Some("tool_roster")
} else if self.provider_request_options.is_some() {
Some("provider_request_options")
} else if self.max_output_tokens.is_some() {
Some("max_output_tokens")
} else if self.compaction.is_some() {
Some("compaction")
} else if self.tool_result_paging.is_some() {
Some("tool_result_paging")
} else if self.system_prompt.is_some() {
Some("system_prompt")
} else {
None
}
}
pub(crate) fn apply_to(
&self,
mut agent: mentra::agent::AgentConfig,
) -> mentra::agent::AgentConfig {
if let Some(roster) = &self.tool_roster {
agent.tool_profile = roster.clone().into_profile();
}
if let Some(options) = &self.provider_request_options {
agent.provider_request_options = options.clone();
}
if let Some(max_output_tokens) = self.max_output_tokens {
agent.max_output_tokens = max_output_tokens;
}
if let Some(compaction) = self.compaction {
let transcript_dir = agent.compaction.transcript_dir.clone();
agent.compaction = compaction.into_mentra(transcript_dir);
}
if let Some(tool_result_paging) = self.tool_result_paging {
agent.tool_result_paging = tool_result_paging;
}
if let Some(system_prompt) = &self.system_prompt {
agent.system = apply_system_prompt(agent.system, system_prompt);
}
agent
}
}
fn set_if<T>(value: Option<&T>) -> Option<&'static str> {
value.map(|_| "<set>")
}
fn redacted_if<T>(value: Option<&T>) -> Option<&'static str> {
value.map(|_| "<redacted>")
}
fn set_or_clear<T>(value: &Option<Option<T>>) -> Option<&'static str> {
value
.as_ref()
.map(|inner| if inner.is_some() { "<set>" } else { "<clear>" })
}
fn apply_system_prompt(current: Option<String>, profile: &SystemPrompt) -> Option<String> {
match profile {
SystemPrompt::Replace(text) => spoken(text),
SystemPrompt::Append(text) => join(current.and_then(|text| spoken(&text)), spoken(text)),
}
}
fn spoken(text: &str) -> Option<String> {
(!text.trim().is_empty()).then(|| text.to_string())
}
fn join(first: Option<String>, second: Option<String>) -> Option<String> {
match (first, second) {
(Some(first), Some(second)) => Some(format!("{first}\n\n{second}")),
(Some(first), None) => Some(first),
(None, Some(second)) => Some(second),
(None, None) => None,
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::path::PathBuf;
use mentra::ReasoningOptions;
use super::*;
use crate::run::Effort;
fn reasoning(effort: Effort) -> ReasoningOptions {
ReasoningOptions {
effort: Some(effort.into()),
summary: None,
}
}
#[test]
fn an_empty_profile_changes_nothing() {
let mut agent = mentra::agent::AgentConfig {
system: Some("workspace".to_string()),
max_output_tokens: Some(777),
tool_result_paging: Some(ToolResultPagingConfig {
threshold_bytes: 10,
page_bytes: 5,
}),
..Default::default()
};
agent.provider_request_options.reasoning = Some(reasoning(Effort::Low));
let applied = RunProfile::new().apply_to(agent.clone());
assert_eq!(
serde_json::to_value(applied).expect("applied config serializes"),
serde_json::to_value(agent).expect("original config serializes")
);
}
#[test]
fn nested_options_distinguish_clear_from_inherit() {
let mut agent = mentra::agent::AgentConfig {
max_output_tokens: Some(777),
tool_result_paging: Some(ToolResultPagingConfig {
threshold_bytes: 10,
page_bytes: 5,
}),
..Default::default()
};
agent.provider_request_options.reasoning = Some(reasoning(Effort::Low));
let cleared = RunProfile::new()
.with_max_output_tokens(None)
.with_tool_result_paging(None)
.apply_to(agent);
assert_eq!(cleared.max_output_tokens, None);
assert_eq!(cleared.tool_result_paging, None);
}
#[test]
fn compaction_and_paging_reach_the_final_agent_without_moving_transcripts() {
let transcript_dir = PathBuf::from("/host/runtime/transcripts");
let agent = mentra::agent::AgentConfig {
compaction: mentra::agent::CompactionConfig {
transcript_dir: transcript_dir.clone(),
..Default::default()
},
..Default::default()
};
let paging = ToolResultPagingConfig {
threshold_bytes: 128 * 1024,
page_bytes: 16 * 1024,
};
let applied = RunProfile::new()
.with_compaction(
Compaction::default()
.with_keep_recent_tool_results(Some(2))
.with_auto_threshold_tokens(Some(12_345))
.with_auto_threshold_percent(Some(60))
.with_preserve_recent_user_tokens(777),
)
.with_tool_result_paging(Some(paging))
.apply_to(agent);
assert_eq!(applied.compaction.keep_recent_tool_results, 2);
assert_eq!(
applied.compaction.auto_compact_threshold_tokens,
Some(12_345)
);
assert_eq!(applied.compaction.auto_compact_threshold_percent, Some(60));
assert_eq!(applied.compaction.preserve_recent_user_tokens, 777);
assert_eq!(applied.compaction.transcript_dir, transcript_dir);
assert_eq!(applied.tool_result_paging, Some(paging));
}
#[test]
fn system_append_keeps_the_workspace_prompt_and_lands_last() {
let agent = mentra::agent::AgentConfig {
system: Some("workspace".to_string()),
..Default::default()
};
let applied = RunProfile::new()
.with_system_prompt(SystemPrompt::Append("host".to_string()))
.apply_to(agent);
assert_eq!(applied.system.as_deref(), Some("workspace\n\nhost"));
}
#[test]
fn stated_request_options_replace_the_agent_reasoning_wholesale() {
let mut agent = mentra::agent::AgentConfig::default();
agent.provider_request_options.reasoning = Some(reasoning(Effort::High));
let mut options = ProviderRequestOptions {
reasoning: Some(reasoning(Effort::Low)),
..Default::default()
};
options.responses.service_tier = Some("priority".to_string());
let applied = RunProfile::new()
.with_provider_request_options(options)
.apply_to(agent);
assert_eq!(
applied.provider_request_options.reasoning,
Some(reasoning(Effort::Low))
);
assert_eq!(
applied
.provider_request_options
.responses
.service_tier
.as_deref(),
Some("priority")
);
}
#[test]
fn debug_redacts_headers_and_system_text() {
let mut options = ProviderRequestOptions::default();
options.session.extra_headers =
BTreeMap::from([("authorization".to_string(), "Bearer secret".to_string())]);
let profile = RunProfile::new()
.with_provider_request_options(options)
.with_system_prompt(SystemPrompt::Replace("private instructions".to_string()));
let rendered = format!("{profile:?}");
assert!(!rendered.contains("Bearer secret"));
assert!(!rendered.contains("private instructions"));
assert!(rendered.contains("<redacted>"));
}
}