use crate::agent::AgentRunError;
use crate::agent::system_prompt::format_skills_for_system_prompt;
use crate::agent::types::{PromptTemplate, Skill};
use super::{AgentHarness, ReloadSkillsError, SessionEvent};
use std::collections::HashSet;
use std::sync::Arc;
pub(super) fn deduplicate_tools(
tools: Vec<Arc<dyn crate::AgentTool>>,
) -> (Vec<Arc<dyn crate::AgentTool>>, Vec<String>) {
let mut seen = HashSet::new();
let mut dropped = Vec::new();
let mut kept = Vec::with_capacity(tools.len());
for tool in tools {
let name = tool.definition().name.clone();
if seen.insert(name.clone()) {
kept.push(tool);
} else {
dropped.push(name);
}
}
(kept, dropped)
}
fn warn_dropped_tools(context: &str, dropped: &[String]) {
if !dropped.is_empty() {
tracing::warn!(
"dropped duplicate tool name(s) {context} (earlier registrations win): {}",
dropped.join(", ")
);
}
}
impl AgentHarness {
pub fn skills(&self) -> Vec<Skill> {
self.skills.lock().clone()
}
pub fn templates(&self) -> Vec<PromptTemplate> {
self.templates.lock().clone()
}
pub fn system_prompt(&self) -> String {
self.agent.state().system_prompt.clone()
}
pub fn replace_skills(&self, skills: Vec<Skill>) {
*self.skills.lock() = skills;
let prompt = build_system_prompt(&self.base_system_prompt, &self.skills.lock());
self.agent.state().system_prompt = prompt;
}
pub fn replace_tools(&self, tools: Vec<Arc<dyn crate::AgentTool>>) {
let (tools, dropped) = deduplicate_tools(tools);
warn_dropped_tools("on tool replacement", &dropped);
self.agent.state().tools = tools;
}
pub fn replace_mcp_tools(
&self,
old: &[Arc<dyn crate::AgentTool>],
new: Vec<Arc<dyn crate::AgentTool>>,
) {
let mut tools = std::mem::take(&mut self.agent.state().tools);
tools.retain(|t| !old.iter().any(|o| Arc::ptr_eq(o, t)));
let mut seen: HashSet<String> = tools.iter().map(|t| t.definition().name.clone()).collect();
let mut dropped: Vec<String> = Vec::new();
for tool in new {
let name = tool.definition().name.clone();
if seen.insert(name.clone()) {
tools.push(tool);
} else {
dropped.push(name);
}
}
warn_dropped_tools("on MCP tool replacement", &dropped);
self.agent.state().tools = tools;
}
pub fn replace_templates(&self, templates: Vec<PromptTemplate>) {
*self.templates.lock() = templates;
}
pub async fn reload_skills_from_disk(
&self,
) -> Result<crate::agent::skills::LoadSkillsOutput, ReloadSkillsError> {
let loader = self
.reload_skills_fn
.as_ref()
.ok_or(ReloadSkillsError::NotConfigured)?
.clone();
let out = loader().await;
self.replace_skills(out.skills.clone());
self.emit_harness_event(SessionEvent::SkillsReloaded {
total: out.skills.len(),
});
Ok(out)
}
pub async fn prompt_from_template(
&self,
name: &str,
vars: serde_json::Map<String, serde_json::Value>,
) -> Result<(), AgentRunError> {
let template = {
let g = self.templates.lock();
g.iter().find(|t| t.name == name).cloned()
};
let template = match template {
Some(t) => t,
None => {
return Err(AgentRunError::Other(format!(
"unknown prompt template: {name}"
)));
}
};
let rendered = template.interpolate(&vars);
self.prompt(rendered).await
}
}
pub(super) fn build_system_prompt(base: &str, skills: &[Skill]) -> String {
let skills_block = format_skills_for_system_prompt(skills);
if base.is_empty() {
return skills_block;
}
if skills_block.is_empty() {
return base.to_string();
}
format!("{base}\n\n{skills_block}")
}