use std::rc::Rc;
use super::json_schema::vm_build_json_schema;
use crate::value::{VmError, VmValue};
pub(crate) fn vm_tools_to_native(
tools_val: &VmValue,
provider: &str,
) -> Result<Vec<serde_json::Value>, VmError> {
let tools_list = match tools_val {
VmValue::Dict(dict) => match dict.get("tools") {
Some(VmValue::List(list)) => list.as_ref().clone(),
_ => Vec::new(),
},
VmValue::List(list) => list.as_ref().clone(),
_ => {
return Err(VmError::Thrown(VmValue::String(Rc::from(
"tools must be a tool_registry or a list of tool definition dicts",
))));
}
};
let mut native_tools = Vec::new();
for tool in &tools_list {
match tool {
VmValue::Dict(entry) => {
let name = entry
.get("name")
.map(|value| value.display())
.unwrap_or_default();
let description = entry
.get("description")
.map(|value| value.display())
.unwrap_or_default();
let params = entry.get("parameters").and_then(|value| value.as_dict());
let output_schema = entry
.get("outputSchema")
.map(super::super::vm_value_to_json);
let defer_loading = matches!(entry.get("defer_loading"), Some(VmValue::Bool(true)));
let namespace = entry.get("namespace").and_then(|value| match value {
VmValue::String(string) if !string.is_empty() => Some(string.to_string()),
_ => None,
});
let input_schema = vm_build_json_schema(params);
let is_anthropic =
super::super::helpers::ResolvedProvider::resolve(provider).is_anthropic_style;
if is_anthropic {
let mut tool_json = serde_json::json!({
"name": name,
"description": description,
"input_schema": input_schema,
});
if let Some(output_schema) = output_schema {
tool_json["x-harn-output-schema"] = output_schema;
}
if defer_loading {
tool_json["defer_loading"] = serde_json::Value::Bool(true);
}
if let Some(ns) = namespace {
tool_json["namespace"] = serde_json::Value::String(ns);
}
native_tools.push(tool_json);
} else {
let mut tool_json = serde_json::json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": input_schema,
}
});
if let Some(output_schema) = output_schema {
tool_json["function"]["x-harn-output-schema"] = output_schema;
}
if defer_loading {
tool_json["defer_loading"] = serde_json::Value::Bool(true);
}
if let Some(ns) = namespace {
tool_json["namespace"] = serde_json::Value::String(ns);
}
native_tools.push(tool_json);
}
}
VmValue::String(_) => {
return Err(VmError::Thrown(VmValue::String(Rc::from(
"tools must be declared as tool definition dicts or a tool_registry",
))));
}
_ => {
return Err(VmError::Thrown(VmValue::String(Rc::from(
"tools must contain only tool definition dicts",
))));
}
}
}
Ok(native_tools)
}
pub(crate) fn extract_deferred_tool_names(native_tools: &[serde_json::Value]) -> Vec<String> {
native_tools
.iter()
.filter_map(|tool| {
if tool.get("defer_loading").and_then(|value| value.as_bool()) == Some(true) {
if let Some(name) = tool.get("name").and_then(|value| value.as_str()) {
return Some(name.to_string());
}
if let Some(name) = tool
.get("function")
.and_then(|function| function.get("name"))
.and_then(|value| value.as_str())
{
return Some(name.to_string());
}
}
None
})
.collect()
}
pub(crate) fn apply_tool_search_client_injection(
native_tools: &mut Option<Vec<serde_json::Value>>,
provider: &str,
cfg: &super::super::api::ToolSearchConfig,
) {
let Some(list) = native_tools.as_mut() else {
return;
};
let always_loaded: std::collections::BTreeSet<&str> =
cfg.always_loaded.iter().map(String::as_str).collect();
list.retain(|tool| {
let is_deferred = tool
.get("defer_loading")
.and_then(|value| value.as_bool())
.unwrap_or(false);
if !is_deferred {
return true;
}
let name = tool
.get("name")
.and_then(|value| value.as_str())
.or_else(|| {
tool.get("function")
.and_then(|function| function.get("name"))
.and_then(|value| value.as_str())
})
.unwrap_or("");
always_loaded.contains(name)
});
for tool in list.iter_mut() {
if let Some(obj) = tool.as_object_mut() {
obj.remove("defer_loading");
}
if let Some(function) = tool
.get_mut("function")
.and_then(|value| value.as_object_mut())
{
function.remove("defer_loading");
}
}
let synthetic = build_client_search_tool_schema(provider, cfg);
list.insert(0, synthetic);
}
pub(crate) fn build_client_search_tool_schema(
provider: &str,
cfg: &super::super::api::ToolSearchConfig,
) -> serde_json::Value {
let name = cfg.effective_name().to_string();
let strategy = cfg.effective_strategy();
let description = match strategy {
super::super::api::ToolSearchStrategy::Regex => {
"Search for tools you need. Pass `query` as a case-insensitive regex \
(Rust `regex` crate syntax — no lookaround, no backreferences). \
The tool returns `{ \"tool_names\": [...] }`; only the returned \
tools will be available to call in the next turn."
}
super::super::api::ToolSearchStrategy::Bm25 => {
"Search for tools you need. Pass `query` as natural-language \
keywords (BM25). The tool returns `{ \"tool_names\": [...] }`; \
only the returned tools will be available to call in the next turn. \
Cast a wider net if the first search returns nothing useful."
}
super::super::api::ToolSearchStrategy::Semantic => {
"Search for tools you need. Pass `query` as a natural-language \
description; a semantic / embedding index returns the best matches \
as `{ \"tool_names\": [...] }`. Only the returned tools will be \
available to call in the next turn."
}
super::super::api::ToolSearchStrategy::Host => {
"Search for tools you need. Pass `query` as the host expects it; \
the host returns `{ \"tool_names\": [...] }`. Only the returned \
tools will be available to call in the next turn."
}
};
let input_schema = serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (keywords for BM25/semantic, regex for regex variant).",
}
},
"required": ["query"],
"additionalProperties": false,
});
let is_anthropic_style =
super::super::helpers::ResolvedProvider::resolve(provider).is_anthropic_style;
if is_anthropic_style {
serde_json::json!({
"name": name,
"description": description,
"input_schema": input_schema,
})
} else {
serde_json::json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": input_schema,
}
})
}
}
pub(crate) fn build_load_skill_tool_schema(provider: &str) -> serde_json::Value {
let description =
"Promote a skill's full body into the next turn's context. Accepts the skill id returned by the always-on catalog.";
let input_schema = serde_json::json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Skill id from the always-on catalog.",
}
},
"required": ["name"],
"additionalProperties": false,
});
let is_anthropic_style =
super::super::helpers::ResolvedProvider::resolve(provider).is_anthropic_style;
if is_anthropic_style {
serde_json::json!({
"name": "load_skill",
"description": description,
"input_schema": input_schema,
})
} else {
serde_json::json!({
"type": "function",
"function": {
"name": "load_skill",
"description": description,
"parameters": input_schema,
}
})
}
}
#[cfg(test)]
pub(crate) fn apply_tool_search_native_injection(
native_tools: &mut Option<Vec<serde_json::Value>>,
provider: &str,
variant: &str,
) {
let shape = if provider == "anthropic" {
super::super::provider::NativeToolSearchShape::Anthropic
} else {
super::super::provider::NativeToolSearchShape::OpenAi
};
apply_tool_search_native_injection_typed(native_tools, shape, variant, "hosted");
}
pub(crate) fn apply_tool_search_native_injection_typed(
native_tools: &mut Option<Vec<serde_json::Value>>,
shape: super::super::provider::NativeToolSearchShape,
variant: &str,
mode: &str,
) {
use super::super::provider::NativeToolSearchShape;
match shape {
NativeToolSearchShape::Anthropic => {
let (type_name, tool_name) = match variant {
"regex" => ("tool_search_tool_regex_20251119", "tool_search_tool_regex"),
_ => ("tool_search_tool_bm25_20251119", "tool_search_tool_bm25"),
};
let meta = serde_json::json!({
"type": type_name,
"name": tool_name,
});
prepend_meta_tool(native_tools, meta);
}
NativeToolSearchShape::OpenAi => {
let resolved_mode = if mode == "client" { "client" } else { "hosted" };
let mut meta = serde_json::json!({
"type": "tool_search",
"mode": resolved_mode,
});
if let Some(tools) = native_tools.as_ref() {
let mut namespaces: Vec<String> = tools.iter().filter_map(tool_namespace).collect();
namespaces.sort();
namespaces.dedup();
if !namespaces.is_empty() {
meta["namespaces"] = serde_json::json!(namespaces);
}
}
prepend_meta_tool(native_tools, meta);
}
}
}
fn prepend_meta_tool(native_tools: &mut Option<Vec<serde_json::Value>>, meta: serde_json::Value) {
match native_tools {
Some(list) => list.insert(0, meta),
None => *native_tools = Some(vec![meta]),
}
}
pub(crate) fn tool_namespace(tool: &serde_json::Value) -> Option<String> {
tool.get("namespace")
.and_then(|value| value.as_str())
.or_else(|| {
tool.get("function")
.and_then(|function| function.get("namespace"))
.and_then(|value| value.as_str())
})
.map(|value| value.to_string())
}