use super::AgentRunner;
use crate::config::types::CapabilityLevel;
use crate::core::agent::harness_kernel::SessionToolCatalogSnapshot;
use crate::llm::provider::{LLMRequest, ToolDefinition};
use crate::tools::handlers::{SessionSurface, SessionToolsConfig, ToolModelCapabilities};
use anyhow::{Result, anyhow};
use hashbrown::HashSet;
impl AgentRunner {
fn session_tools_config_for_snapshot(
&self,
planning_active: bool,
request_user_input_enabled: bool,
) -> SessionToolsConfig {
SessionToolsConfig {
surface: SessionSurface::AgentRunner,
capability_level: CapabilityLevel::CodeSearch,
documentation_mode: self.config().agent.tool_documentation_mode,
planning_active,
request_user_input_enabled,
model_capabilities: ToolModelCapabilities::for_model_name(&self.model),
deferred_tool_policy: crate::tools::handlers::deferred_tool_policy_for_runtime(
crate::llm::factory::infer_provider(Some(&self.config().agent.provider), &self.model),
self.provider_client.supports_responses_compaction(&self.model),
Some(self.config()),
),
anthropic_native_memory_enabled: crate::tools::handlers::anthropic_native_memory_enabled_for_runtime(
crate::llm::factory::infer_provider(Some(&self.config().agent.provider), &self.model),
&self.model,
Some(self.config()),
),
tool_profile: self.config().tools.profile,
}
}
pub(super) async fn build_exposed_tool_snapshot(&self) -> Result<SessionToolCatalogSnapshot> {
let planning_active = self.tool_registry.is_planning_active();
let request_user_input_enabled = false;
let stable_config = self.session_tools_config_for_snapshot(true, true);
let definitions = self.tool_registry.model_tools(stable_config).await;
let runtime_config = self.session_tools_config_for_snapshot(planning_active, false);
let mut runtime_tool_names: HashSet<String> = self
.tool_registry
.model_tools(runtime_config)
.await
.into_iter()
.map(|tool| tool.function_name().to_string())
.collect();
if let Some(full_auto_allowlist) = self.tool_registry.current_full_auto_allowlist().await {
runtime_tool_names.extend(full_auto_allowlist);
}
let supports_active_name_restriction = self.provider_client.supports_native_allowed_tools(&self.model);
let mut active_tool_names = Vec::new();
let mut exposed_definitions = Vec::new();
for tool in definitions {
let tool_name = tool.function_name().to_string();
if !self.is_tool_permitted_for_advertisement(tool_name.as_str()).await {
continue;
}
let active = runtime_tool_names.contains(&tool_name) && self.is_tool_exposed(tool_name.as_str()).await;
if active {
active_tool_names.push(tool_name);
}
if supports_active_name_restriction || active {
exposed_definitions.push(tool);
}
}
Ok(self
.tool_registry
.tool_catalog_state()
.snapshot_for_stable_defs_with_active_names(
exposed_definitions,
active_tool_names,
planning_active,
request_user_input_enabled,
))
}
pub(super) async fn build_exposed_tool_definitions(&self) -> Result<Vec<ToolDefinition>> {
let snapshot = self.build_exposed_tool_snapshot().await?;
let active_tool_names: HashSet<&str> = snapshot.active_tool_names.iter().map(String::as_str).collect();
Ok(snapshot
.snapshot
.as_ref()
.map(|defs| {
defs.iter()
.filter(|tool| active_tool_names.contains(tool.function_name()))
.cloned()
.collect()
})
.unwrap_or_default())
}
pub(crate) async fn build_universal_tools(&self) -> Result<Vec<ToolDefinition>> {
if let Some(definitions) = self.tool_definitions_override.read().clone() {
return Ok(definitions);
}
self.build_exposed_tool_definitions().await
}
pub(crate) async fn build_universal_tool_snapshot(&self) -> Result<SessionToolCatalogSnapshot> {
if let Some(definitions) = self.tool_definitions_override.read().clone() {
return Ok(self.tool_registry.tool_catalog_state().snapshot_for_defs(
definitions,
self.tool_registry.is_planning_active(),
false,
));
}
self.build_exposed_tool_snapshot().await
}
pub(super) fn validate_llm_request(&self, request: &LLMRequest) -> Result<()> {
if request.system_prompt.as_ref().is_none_or(|s| s.trim().is_empty()) {
return Err(anyhow!("system prompt cannot be empty"));
}
if request.messages.is_empty() {
return Err(anyhow!("message history cannot be empty"));
}
if request.model.trim().is_empty() {
return Err(anyhow!("model identifier cannot be empty"));
}
if let Some(tools) = request.tools.as_deref() {
let mut seen: HashSet<&str> = HashSet::with_capacity(tools.len());
for tool in tools {
validate_tool_definition(tool, &mut seen)?;
}
}
Ok(())
}
}
fn validate_tool_definition<'a>(tool: &'a ToolDefinition, seen: &mut HashSet<&'a str>) -> Result<()> {
let Some(func) = tool.function.as_ref() else {
return Ok(());
};
let name = func.name.as_str();
if name.trim().is_empty() {
return Err(anyhow!("tool function name cannot be empty"));
}
if !seen.insert(name) {
return Err(anyhow!("duplicate tool name: {name}"));
}
if let Some(obj) = func.parameters.as_object()
&& let Some(required_arr) = obj.get("required").and_then(|v| v.as_array())
&& let Some(props) = obj.get("properties").and_then(|p| p.as_object())
{
for req in required_arr.iter().filter_map(|v| v.as_str()) {
if !props.contains_key(req) {
return Err(anyhow!("tool '{name}' has required field '{req}' not in properties"));
}
}
}
Ok(())
}