use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use super::vm_value_to_json;
use crate::value::{VmError, VmValue};
pub(crate) fn build_assistant_tool_message(
text: &str,
tool_calls: &[serde_json::Value],
provider: &str,
) -> serde_json::Value {
match provider {
"openai" | "openrouter" => {
let calls: Vec<serde_json::Value> = tool_calls
.iter()
.map(|tc| {
serde_json::json!({
"id": tc["id"],
"type": "function",
"function": {
"name": tc["name"],
"arguments": serde_json::to_string(&tc["arguments"]).unwrap_or_default(),
}
})
})
.collect();
let mut msg = serde_json::json!({
"role": "assistant",
"tool_calls": calls,
});
if !text.is_empty() {
msg["content"] = serde_json::json!(text);
}
msg
}
_ => {
let mut content = Vec::new();
if !text.is_empty() {
content.push(serde_json::json!({"type": "text", "text": text}));
}
for tc in tool_calls {
content.push(serde_json::json!({
"type": "tool_use",
"id": tc["id"],
"name": tc["name"],
"input": tc["arguments"],
}));
}
serde_json::json!({"role": "assistant", "content": content})
}
}
}
pub(crate) fn build_assistant_response_message(
text: &str,
blocks: &[serde_json::Value],
tool_calls: &[serde_json::Value],
provider: &str,
) -> serde_json::Value {
if !tool_calls.is_empty() {
return build_assistant_tool_message(text, tool_calls, provider);
}
if !blocks.is_empty() {
return serde_json::json!({
"role": "assistant",
"content": blocks,
});
}
serde_json::json!({
"role": "assistant",
"content": text,
})
}
pub(crate) fn build_tool_result_message(
tool_call_id: &str,
result: &str,
provider: &str,
) -> serde_json::Value {
match provider {
"openai" | "openrouter" => {
serde_json::json!({
"role": "tool",
"tool_call_id": tool_call_id,
"content": result,
})
}
_ => {
serde_json::json!({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": tool_call_id,
"content": result,
}]
})
}
}
}
pub(crate) fn normalize_tool_args(name: &str, args: &serde_json::Value) -> serde_json::Value {
let mut obj = match args.as_object() {
Some(o) => o.clone(),
None => return args.clone(),
};
if name == "edit" {
if !obj.contains_key("action") {
if let Some(v) = obj.remove("mode").or_else(|| obj.remove("command")) {
obj.insert("action".to_string(), v);
}
}
let action = obj
.get("action")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if action == "patch" || action == "replace" {
if !obj.contains_key("old_string") {
if let Some(v) = obj.remove("find") {
obj.insert("old_string".to_string(), v);
}
}
if !obj.contains_key("new_string") {
if let Some(v) = obj.remove("content") {
obj.insert("new_string".to_string(), v);
}
}
}
if !obj.contains_key("path") {
if let Some(v) = obj.remove("file") {
obj.insert("path".to_string(), v);
}
}
}
if name == "run" || name == "exec" {
if !obj.contains_key("command") {
if let Some(v) = obj.remove("args").or_else(|| obj.remove("argv")) {
obj.insert("command".to_string(), v);
}
}
let command_value = obj.get("command").cloned();
let args_value = obj
.get("args")
.cloned()
.or_else(|| obj.get("argv").cloned());
if let Some(command) = normalize_run_command(command_value.as_ref(), args_value.as_ref()) {
obj.insert("command".to_string(), serde_json::json!(command));
}
obj.remove("args");
obj.remove("argv");
}
serde_json::Value::Object(obj)
}
fn normalize_run_command(
command_value: Option<&serde_json::Value>,
fallback_value: Option<&serde_json::Value>,
) -> Option<String> {
let command_parts = command_value
.and_then(run_command_tokens)
.unwrap_or_default();
let fallback_parts = fallback_value
.and_then(run_command_tokens)
.unwrap_or_default();
let parts = if command_parts.is_empty() {
fallback_parts
} else if fallback_parts.is_empty() {
command_parts
} else {
let mut combined = fallback_parts;
combined.extend(command_parts);
combined
};
if parts.is_empty() {
None
} else {
Some(parts.join(" "))
}
}
fn run_command_tokens(value: &serde_json::Value) -> Option<Vec<String>> {
match value {
serde_json::Value::Array(parts) => {
let tokens = parts
.iter()
.filter_map(|part| part.as_str())
.map(|part| part.trim().to_string())
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
(!tokens.is_empty()).then_some(tokens)
}
serde_json::Value::String(text) => run_command_tokens_from_str(text),
_ => None,
}
}
fn run_command_tokens_from_str(text: &str) -> Option<Vec<String>> {
let trimmed = text.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
if let Ok(parts) = serde_json::from_str::<Vec<String>>(trimmed) {
let tokens = parts
.into_iter()
.map(|part| part.trim().to_string())
.filter(|part| !part.is_empty())
.collect::<Vec<_>>();
if !tokens.is_empty() {
return Some(tokens);
}
}
}
if (trimmed.contains('[') || trimmed.contains(']') || trimmed.contains("\\\""))
&& trimmed.contains('"')
{
let tokens = extract_quoted_tokens(trimmed);
if !tokens.is_empty() {
return Some(tokens);
}
}
Some(vec![trimmed.to_string()])
}
fn extract_quoted_tokens(text: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut in_quote = false;
let mut escape = false;
for ch in text.chars() {
if !in_quote {
if ch == '"' {
in_quote = true;
current.clear();
}
continue;
}
if escape {
current.push(ch);
escape = false;
continue;
}
match ch {
'\\' => escape = true,
'"' => {
if !current.trim().is_empty() {
tokens.push(current.trim().to_string());
}
current.clear();
in_quote = false;
}
_ => current.push(ch),
}
}
tokens
}
fn resolve_local_tool_path(path: &str) -> std::path::PathBuf {
let candidate = std::path::PathBuf::from(path);
if candidate.is_absolute() {
return candidate;
}
if let Some(cwd) =
crate::stdlib::process::current_execution_context().and_then(|context| context.cwd)
{
return std::path::PathBuf::from(cwd).join(candidate);
}
crate::stdlib::process::resolve_source_relative_path(path)
}
pub(crate) fn handle_tool_locally(name: &str, args: &serde_json::Value) -> Option<String> {
match name {
"read_file" | "read" => {
let path = args
.get("path")
.or_else(|| args.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("");
if path.is_empty() {
return Some("Error: missing path parameter".to_string());
}
let resolved = resolve_local_tool_path(path);
if resolved.is_dir() {
return match std::fs::read_dir(&resolved) {
Ok(entries) => {
let mut names: Vec<String> = entries
.filter_map(|e| e.ok())
.map(|e| {
let name = e.file_name().to_string_lossy().to_string();
if e.path().is_dir() {
format!("{}/", name)
} else {
name
}
})
.collect();
names.sort();
Some(names.join("\n"))
}
Err(e) => Some(format!("Error: cannot list directory '{}': {}", path, e)),
};
}
match std::fs::read_to_string(&resolved) {
Ok(content) => {
let numbered: String = content
.lines()
.enumerate()
.map(|(i, line)| format!("{}\t{}", i + 1, line))
.collect::<Vec<_>>()
.join("\n");
Some(numbered)
}
Err(e) => Some(format!("Error: cannot read file '{}': {}", path, e)),
}
}
"list_directory" => {
let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
let resolved = resolve_local_tool_path(path);
match std::fs::read_dir(&resolved) {
Ok(entries) => {
let mut names: Vec<String> = entries
.filter_map(|e| e.ok())
.map(|e| {
let name = e.file_name().to_string_lossy().to_string();
if e.path().is_dir() {
format!("{}/", name)
} else {
name
}
})
.collect();
names.sort();
Some(names.join("\n"))
}
Err(e) => Some(format!("Error: cannot list directory '{}': {}", path, e)),
}
}
_ => None,
}
}
#[derive(Clone, Debug, serde::Serialize)]
pub(crate) enum TypeExpr {
Primitive(String),
Literal(serde_json::Value),
Array(Box<TypeExpr>),
Union(Vec<TypeExpr>),
Intersection(Vec<TypeExpr>),
Object(Vec<ObjectField>),
Ref(String),
Unknown,
}
#[derive(Clone, Debug, serde::Serialize)]
pub(crate) struct ObjectField {
pub(crate) name: String,
pub(crate) ty: TypeExpr,
pub(crate) required: bool,
pub(crate) description: Option<String>,
pub(crate) default: Option<serde_json::Value>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ComponentRegistry {
types: BTreeMap<String, TypeExpr>,
order: Vec<String>,
in_progress: BTreeSet<String>,
}
impl ComponentRegistry {
fn register(&mut self, name: String, ty: TypeExpr) {
if !self.types.contains_key(&name) {
self.order.push(name.clone());
}
self.types.insert(name, ty);
}
fn contains(&self, name: &str) -> bool {
self.types.contains_key(name)
}
pub(crate) fn render_aliases(&self) -> String {
if self.order.is_empty() {
return String::new();
}
let mut out = String::new();
for name in &self.order {
if let Some(ty) = self.types.get(name) {
out.push_str(&format!("type {} = {};\n", name, ty.render()));
}
}
out
}
}
fn ref_name_from_pointer(pointer: &str) -> Option<String> {
let stripped = pointer.trim_start_matches('#').trim_start_matches('/');
let last = stripped.rsplit('/').next()?;
if last.is_empty() {
None
} else {
Some(last.to_string())
}
}
fn resolve_json_ref<'a>(
root: &'a serde_json::Value,
pointer: &str,
) -> Option<&'a serde_json::Value> {
let stripped = pointer.trim_start_matches('#').trim_start_matches('/');
if stripped.is_empty() {
return Some(root);
}
let mut current = root;
for segment in stripped.split('/') {
let decoded = segment.replace("~1", "/").replace("~0", "~");
current = match current {
serde_json::Value::Object(obj) => obj.get(&decoded)?,
serde_json::Value::Array(arr) => {
let idx: usize = decoded.parse().ok()?;
arr.get(idx)?
}
_ => return None,
};
}
Some(current)
}
impl TypeExpr {
pub(crate) fn render(&self) -> String {
match self {
TypeExpr::Primitive(name) => normalize_primitive_name(name).to_string(),
TypeExpr::Literal(value) => render_literal(value),
TypeExpr::Array(inner) => {
match inner.as_ref() {
TypeExpr::Union(_) | TypeExpr::Intersection(_) => {
format!("({})[]", inner.render())
}
_ => format!("{}[]", inner.render()),
}
}
TypeExpr::Union(members) => members
.iter()
.map(|m| m.render())
.collect::<Vec<_>>()
.join(" | "),
TypeExpr::Intersection(members) => members
.iter()
.map(|m| {
let rendered = m.render();
if matches!(m, TypeExpr::Union(_)) {
format!("({rendered})")
} else {
rendered
}
})
.collect::<Vec<_>>()
.join(" & "),
TypeExpr::Object(fields) => {
if fields.is_empty() {
"{}".to_string()
} else {
let rendered = fields
.iter()
.map(|f| {
let marker = if f.required { "" } else { "?" };
format!("{}{}: {}", f.name, marker, f.ty.render())
})
.collect::<Vec<_>>()
.join("; ");
format!("{{ {rendered} }}")
}
}
TypeExpr::Ref(name) => name.clone(),
TypeExpr::Unknown => "unknown".to_string(),
}
}
}
fn normalize_primitive_name(raw: &str) -> &str {
match raw {
"str" | "string" => "string",
"int" | "integer" | "long" | "number" | "float" | "double" => "number",
"bool" | "boolean" => "boolean",
"nil" | "null" | "none" => "null",
"dict" | "map" => "object",
"list" | "array" => "unknown[]", "any" => "any",
"void" => "void",
other => other,
}
}
fn render_literal(value: &serde_json::Value) -> String {
match value {
serde_json::Value::String(s) => {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Null => "null".to_string(),
other => other.to_string(),
}
}
fn json_schema_to_type_expr(
schema: &serde_json::Value,
root: &serde_json::Value,
registry: &mut ComponentRegistry,
) -> TypeExpr {
let obj = match schema.as_object() {
Some(obj) => obj,
None => {
if let Some(s) = schema.as_str() {
return TypeExpr::Primitive(s.to_string());
}
return TypeExpr::Unknown;
}
};
if let Some(serde_json::Value::String(pointer)) = obj.get("$ref") {
if let Some(name) = ref_name_from_pointer(pointer) {
if !registry.contains(&name) && !registry.in_progress.contains(&name) {
if let Some(resolved) = resolve_json_ref(root, pointer) {
registry.in_progress.insert(name.clone());
let expanded = json_schema_to_type_expr(resolved, root, registry);
registry.in_progress.remove(&name);
registry.register(name.clone(), expanded);
}
}
return TypeExpr::Ref(name);
}
return TypeExpr::Unknown;
}
if let Some(c) = obj.get("const") {
return TypeExpr::Literal(c.clone());
}
if let Some(serde_json::Value::Array(values)) = obj.get("enum") {
let members: Vec<TypeExpr> = values
.iter()
.map(|v| TypeExpr::Literal(v.clone()))
.collect();
return match members.len() {
0 => TypeExpr::Unknown,
1 => members.into_iter().next().unwrap(),
_ => TypeExpr::Union(members),
};
}
for key in ["oneOf", "anyOf"] {
if let Some(serde_json::Value::Array(variants)) = obj.get(key) {
let members: Vec<TypeExpr> = variants
.iter()
.map(|v| json_schema_to_type_expr(v, root, registry))
.filter(|t| !matches!(t, TypeExpr::Unknown))
.collect();
return match members.len() {
0 => TypeExpr::Unknown,
1 => members.into_iter().next().unwrap(),
_ => merge_nullable(TypeExpr::Union(members)),
};
}
}
if let Some(serde_json::Value::Array(variants)) = obj.get("allOf") {
let members: Vec<TypeExpr> = variants
.iter()
.map(|v| json_schema_to_type_expr(v, root, registry))
.filter(|t| !matches!(t, TypeExpr::Unknown))
.collect();
return match members.len() {
0 => TypeExpr::Unknown,
1 => members.into_iter().next().unwrap(),
_ => TypeExpr::Intersection(members),
};
}
let nullable = obj
.get("nullable")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let core_type = match obj.get("type") {
Some(serde_json::Value::Array(type_list)) => {
let primitives: Vec<TypeExpr> = type_list
.iter()
.filter_map(|v| v.as_str().map(|s| TypeExpr::Primitive(s.to_string())))
.collect();
match primitives.len() {
0 => TypeExpr::Unknown,
1 => primitives.into_iter().next().unwrap(),
_ => TypeExpr::Union(primitives),
}
}
Some(serde_json::Value::String(t)) => match t.as_str() {
"array" => {
let item_schema = obj.get("items").cloned().unwrap_or(serde_json::json!({}));
let item_type = json_schema_to_type_expr(&item_schema, root, registry);
TypeExpr::Array(Box::new(item_type))
}
"object" => {
if let Some(props) = obj.get("properties").and_then(|v| v.as_object()) {
let required_set: BTreeSet<String> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
let mut fields: Vec<ObjectField> = props
.iter()
.map(|(name, sub_schema)| ObjectField {
name: name.clone(),
ty: json_schema_to_type_expr(sub_schema, root, registry),
required: required_set.contains(name),
description: sub_schema
.get("description")
.and_then(|v| v.as_str())
.map(str::to_string),
default: sub_schema.get("default").cloned(),
})
.collect();
fields.sort_by_key(|f| !f.required);
TypeExpr::Object(fields)
} else {
TypeExpr::Primitive("object".to_string())
}
}
other => TypeExpr::Primitive(other.to_string()),
},
_ => TypeExpr::Unknown,
};
if nullable {
merge_nullable(TypeExpr::Union(vec![
core_type,
TypeExpr::Primitive("null".to_string()),
]))
} else {
core_type
}
}
fn merge_nullable(ty: TypeExpr) -> TypeExpr {
if let TypeExpr::Union(ref members) = ty {
let null_count = members
.iter()
.filter(|m| matches!(m, TypeExpr::Primitive(name) if name == "null"))
.count();
if null_count <= 1 {
return ty;
}
let mut seen_null = false;
let deduped: Vec<TypeExpr> = members
.iter()
.filter(|m| match m {
TypeExpr::Primitive(name) if name == "null" => {
if seen_null {
false
} else {
seen_null = true;
true
}
}
_ => true,
})
.cloned()
.collect();
return TypeExpr::Union(deduped);
}
ty
}
fn extract_params_from_vm_dict(
td: &BTreeMap<String, VmValue>,
root_json: &serde_json::Value,
registry: &mut ComponentRegistry,
) -> Vec<ToolParamSchema> {
let mut params = Vec::new();
if let Some(VmValue::Dict(pd)) = td.get("parameters") {
for (pname, pval) in pd.iter() {
let (ty, desc, required, default, examples) = if let VmValue::Dict(pdef) = pval {
let desc = pdef
.get("description")
.map(|v| v.display())
.unwrap_or_default();
let required = match pdef.get("required") {
Some(VmValue::Bool(b)) => *b,
_ => true,
};
let json = vm_dict_to_json(pdef);
let ty = json_schema_to_type_expr(&json, root_json, registry);
let default = json.get("default").cloned();
let examples = extract_examples_vm(pdef);
(ty, desc, required, default, examples)
} else {
(
TypeExpr::Primitive("string".to_string()),
pval.display(),
true,
None,
Vec::new(),
)
};
params.push(ToolParamSchema {
name: pname.clone(),
ty,
description: desc,
required,
default,
examples,
});
}
}
params.sort_by_key(|p| !p.required);
params
}
fn vm_dict_to_json(dict: &BTreeMap<String, VmValue>) -> serde_json::Value {
vm_value_to_json(&VmValue::Dict(Rc::new(dict.clone())))
}
#[derive(Clone, Debug, serde::Serialize)]
pub(crate) struct ToolParamSchema {
pub(crate) name: String,
pub(crate) ty: TypeExpr,
pub(crate) description: String,
pub(crate) required: bool,
pub(crate) default: Option<serde_json::Value>,
pub(crate) examples: Vec<serde_json::Value>,
}
impl ToolParamSchema {
fn rendered_default_suffix(&self) -> String {
match &self.default {
Some(v) => format!(" = {}", render_literal(v)),
None => String::new(),
}
}
fn rendered_examples_suffix(&self) -> String {
if self.examples.is_empty() {
return String::new();
}
let rendered = self
.examples
.iter()
.map(render_literal)
.collect::<Vec<_>>()
.join(", ");
if self.examples.len() == 1 {
format!(" Example: {rendered}.")
} else {
format!(" Examples: {rendered}.")
}
}
}
fn extract_examples(obj: &serde_json::Map<String, serde_json::Value>) -> Vec<serde_json::Value> {
if let Some(serde_json::Value::Array(arr)) = obj.get("examples") {
return arr.clone();
}
if let Some(single) = obj.get("example") {
return vec![single.clone()];
}
Vec::new()
}
fn extract_examples_vm(pdef: &BTreeMap<String, VmValue>) -> Vec<serde_json::Value> {
if let Some(VmValue::List(items)) = pdef.get("examples") {
return items.iter().map(vm_value_to_json).collect();
}
if let Some(single) = pdef.get("example") {
return vec![vm_value_to_json(single)];
}
Vec::new()
}
#[derive(Clone, Debug, serde::Serialize)]
pub(crate) struct ToolSchema {
name: String,
description: String,
params: Vec<ToolParamSchema>,
}
fn collect_vm_tool_schemas(
tools_val: Option<&VmValue>,
registry: &mut ComponentRegistry,
) -> Vec<ToolSchema> {
let root_json = match tools_val {
Some(value) => vm_value_to_json(value),
None => serde_json::Value::Null,
};
let entries: Vec<&VmValue> = match tools_val {
Some(VmValue::List(list)) => list.iter().collect(),
Some(VmValue::Dict(d)) => {
if let Some(VmValue::List(tools)) = d.get("tools") {
tools.iter().collect()
} else {
Vec::new()
}
}
_ => Vec::new(),
};
entries
.into_iter()
.filter_map(|value| match value {
VmValue::Dict(td) => {
let name = td.get("name")?.display();
let description = td
.get("description")
.map(|v| v.display())
.unwrap_or_default();
let params = extract_params_from_vm_dict(td, &root_json, registry);
Some(ToolSchema {
name,
description,
params,
})
}
_ => None,
})
.collect()
}
fn schema_description_from_json(value: &serde_json::Value) -> String {
value
.as_str()
.map(ToString::to_string)
.or_else(|| {
value
.get("description")
.and_then(|inner| inner.as_str())
.map(ToString::to_string)
})
.unwrap_or_default()
}
fn extract_params_from_native_schema(
input_schema: &serde_json::Value,
root: &serde_json::Value,
registry: &mut ComponentRegistry,
) -> Vec<ToolParamSchema> {
let required_set: BTreeSet<String> = input_schema
.get("required")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
input_schema
.get("properties")
.and_then(|value| value.as_object())
.map(|properties| {
let mut params = properties
.iter()
.map(|(name, value)| {
let examples = value.as_object().map(extract_examples).unwrap_or_default();
ToolParamSchema {
name: name.clone(),
ty: json_schema_to_type_expr(value, root, registry),
description: schema_description_from_json(value),
required: required_set.contains(name),
default: value.get("default").cloned(),
examples,
}
})
.collect::<Vec<_>>();
params.sort_by(|a, b| {
(!a.required)
.cmp(&!b.required)
.then_with(|| a.name.cmp(&b.name))
});
params
})
.unwrap_or_default()
}
fn collect_native_tool_schemas(
native_tools: Option<&[serde_json::Value]>,
registry: &mut ComponentRegistry,
) -> Vec<ToolSchema> {
native_tools
.unwrap_or(&[])
.iter()
.filter_map(|tool| {
let function = tool.get("function");
let name = function
.and_then(|value| value.get("name"))
.or_else(|| tool.get("name"))
.and_then(|value| value.as_str())?;
let description = function
.and_then(|value| value.get("description"))
.or_else(|| tool.get("description"))
.and_then(|value| value.as_str())
.unwrap_or_default()
.to_string();
let input_schema = function
.and_then(|value| value.get("parameters"))
.or_else(|| tool.get("input_schema"))
.cloned()
.unwrap_or_else(|| serde_json::json!({"type": "object"}));
let root = tool.clone();
Some(ToolSchema {
name: name.to_string(),
description,
params: extract_params_from_native_schema(&input_schema, &root, registry),
})
})
.collect()
}
pub(crate) fn collect_tool_schemas_with_registry(
tools_val: Option<&VmValue>,
native_tools: Option<&[serde_json::Value]>,
) -> (Vec<ToolSchema>, ComponentRegistry) {
let mut registry = ComponentRegistry::default();
let mut merged = collect_vm_tool_schemas(tools_val, &mut registry);
let mut seen = merged
.iter()
.map(|schema| schema.name.clone())
.collect::<BTreeSet<_>>();
for schema in collect_native_tool_schemas(native_tools, &mut registry) {
if seen.insert(schema.name.clone()) {
merged.push(schema);
}
}
merged.sort_by(|a, b| a.name.cmp(&b.name));
(merged, registry)
}
pub(crate) fn collect_tool_schemas(
tools_val: Option<&VmValue>,
native_tools: Option<&[serde_json::Value]>,
) -> Vec<ToolSchema> {
collect_tool_schemas_with_registry(tools_val, native_tools).0
}
pub(crate) fn build_tool_calling_contract_prompt(
tools_val: Option<&VmValue>,
native_tools: Option<&[serde_json::Value]>,
mode: &str,
include_format: bool,
) -> String {
let mut prompt = String::from("\n\n## Tool Calling Contract\n");
prompt.push_str(&format!(
"Active mode: `{mode}`. Follow this runtime-owned contract even if older prompt text suggests another tool syntax.\n\n"
));
let (schemas, registry) = collect_tool_schemas_with_registry(tools_val, native_tools);
let aliases = registry.render_aliases();
if !aliases.is_empty() {
prompt.push_str("## Shared types\n\n");
prompt.push_str(&aliases);
prompt.push('\n');
}
prompt.push_str("## Available tools\n\n");
for schema in &schemas {
let args_type = build_tool_args_type(&schema.params);
prompt.push_str(&format!(
"declare function {}(args: {}): string;\n",
schema.name,
args_type.render()
));
if !schema.description.trim().is_empty() {
prompt.push_str("/**\n");
for line in schema.description.lines() {
prompt.push_str(&format!(" * {line}\n"));
}
for p in schema.params.iter() {
let tag = if p.required { "required" } else { "optional" };
let default_suffix = p.rendered_default_suffix();
let examples_suffix = p.rendered_examples_suffix();
if p.description.is_empty()
&& default_suffix.is_empty()
&& examples_suffix.is_empty()
{
continue;
}
prompt.push_str(&format!(
" * @param {} ({tag}){}{} {}{}\n",
p.name,
if default_suffix.is_empty() { "" } else { " " },
default_suffix,
if p.description.is_empty() {
"".to_string()
} else {
format!("— {}", p.description.trim())
},
examples_suffix,
));
}
prompt.push_str(" */\n");
} else if schema.params.iter().any(|p| !p.description.is_empty()) {
prompt.push_str("/**\n");
for p in schema.params.iter() {
if p.description.is_empty() {
continue;
}
let tag = if p.required { "required" } else { "optional" };
let examples_suffix = p.rendered_examples_suffix();
prompt.push_str(&format!(
" * @param {} ({tag}) — {}{}\n",
p.name,
p.description.trim(),
examples_suffix,
));
}
prompt.push_str(" */\n");
}
prompt.push('\n');
}
if mode == "native" {
prompt.push_str("Use the provider's native tool-calling channel for tool invocations.\n");
} else if include_format {
prompt.push_str(TS_CALL_CONTRACT_HELP);
}
prompt
}
fn build_tool_args_type(params: &[ToolParamSchema]) -> TypeExpr {
let fields: Vec<ObjectField> = params
.iter()
.map(|p| ObjectField {
name: p.name.clone(),
ty: p.ty.clone(),
required: p.required,
description: if p.description.is_empty() {
None
} else {
Some(p.description.clone())
},
default: p.default.clone(),
})
.collect();
TypeExpr::Object(fields)
}
pub(crate) const TS_CALL_CONTRACT_HELP: &str = "
## How to call tools
You invoke a tool by writing a plain TypeScript function call on its own line in your response. The call takes exactly one argument: an object literal whose fields match the tool's signature above. You can write prose before and after tool calls freely — anything that is not a top-of-line function-call expression naming a known tool is narration and the harness ignores it.
Here is an example of calling the edit tool, with a single multiline string argument expressed as a template literal (opened and closed by single backtick characters):
edit({
action: \"create\",
path: \"internal/manifest/parser_extra_test.go\",
content: `package manifest
import \"testing\"
func TestParseExtra(t *testing.T) {
\t// body
}
`
})
Rules for the call expression:
- The full form is name({ key: value, key: value }). Trailing commas are optional.
- String values can be written with double quotes (\"...\") for single-line text, or with a template literal (opened and closed by a single backtick character) for multiline text. Template literal content is passed through raw — you do NOT need to escape newlines, double quotes, or backslashes. If you genuinely need a literal backtick character inside a template literal, escape it with a leading backslash.
- Optional fields may be omitted entirely; required fields must be present (their type has no trailing ? in the signature).
- Enum / literal fields accept only the literal values shown in the union type. Other strings are rejected.
- Arrays use [a, b, c]. Nested objects use { key: value }.
- Tool-name mentions inside a Markdown fenced code block or inside backtick-delimited inline code are treated as documentation, not invocations. Only calls outside every code-display region count.
- After each call, you will see a <tool_result name=\"...\">...</tool_result> entry with the outcome before your next turn.
- You can emit multiple tool calls in one response by placing each as its own top-of-line expression.
";
pub(crate) struct TextToolParseResult {
pub calls: Vec<serde_json::Value>,
pub errors: Vec<String>,
pub prose: String,
}
pub(crate) fn parse_text_tool_calls_with_tools(
text: &str,
tools_val: Option<&VmValue>,
) -> TextToolParseResult {
if let Some(unwrapped) = unwrap_exact_code_wrapper(text) {
let result = parse_text_tool_calls_with_tools(unwrapped, tools_val);
if !result.calls.is_empty() || !result.errors.is_empty() {
return result;
}
}
let known: BTreeSet<String> = collect_tool_schemas(tools_val, None)
.into_iter()
.map(|s| s.name)
.collect();
let mut calls = Vec::new();
let mut errors = Vec::new();
let mut call_ranges: Vec<(usize, usize)> = Vec::new();
let bytes = text.as_bytes();
let mut i = 0usize;
let mut at_line_start = true;
let mut in_fence = false;
let mut in_inline_code = false;
while i < bytes.len() {
if at_line_start && !in_inline_code {
let mut j = i;
while j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
if bytes.get(j) == Some(&b'`')
&& bytes.get(j + 1) == Some(&b'`')
&& bytes.get(j + 2) == Some(&b'`')
{
in_fence = !in_fence;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
if i < bytes.len() {
i += 1;
}
at_line_start = true;
continue;
}
if !in_fence {
if let Some(name_len) = ident_length(&bytes[j..]) {
if bytes.get(j + name_len) == Some(&b'(')
&& known
.contains(std::str::from_utf8(&bytes[j..j + name_len]).unwrap_or(""))
{
let name = std::str::from_utf8(&bytes[j..j + name_len])
.unwrap()
.to_string();
match parse_ts_call_from(&text[j..], name.clone()) {
Ok((arguments, consumed)) => {
calls.push(serde_json::json!({
"id": format!("tc_{}", calls.len()),
"name": name,
"arguments": arguments,
}));
call_ranges.push((j, j + consumed));
i = j + consumed;
at_line_start = bytes.get(i.saturating_sub(1)) == Some(&b'\n');
continue;
}
Err(msg) => {
errors.push(msg);
i = j + name_len + 1;
at_line_start = false;
continue;
}
}
}
}
}
}
if in_fence {
at_line_start = bytes[i] == b'\n';
i += 1;
continue;
}
if bytes[i] == b'`' {
in_inline_code = !in_inline_code;
at_line_start = false;
i += 1;
continue;
}
if bytes[i] == b'\n' {
at_line_start = true;
} else if !bytes[i].is_ascii_whitespace() {
at_line_start = false;
}
i += 1;
}
let prose = if call_ranges.is_empty() {
text.to_string()
} else {
let mut buf = String::with_capacity(text.len());
let mut cursor = 0usize;
for (start, end) in &call_ranges {
if *start > cursor {
buf.push_str(&text[cursor..*start]);
}
cursor = *end;
}
if cursor < text.len() {
buf.push_str(&text[cursor..]);
}
collapse_blank_lines(&buf).trim().to_string()
};
TextToolParseResult {
calls,
errors,
prose,
}
}
fn unwrap_exact_code_wrapper(text: &str) -> Option<&str> {
let trimmed = text.trim();
if let Some(rest) = trimmed.strip_prefix("```") {
let newline = rest.find('\n')?;
let after_opener = &rest[newline + 1..];
let inner = after_opener.strip_suffix("```")?;
return Some(inner.trim());
}
let inner = trimmed.strip_prefix('`')?.strip_suffix('`')?;
if inner.contains('`') {
return None;
}
Some(inner.trim())
}
fn collapse_blank_lines(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut newline_run = 0usize;
for ch in text.chars() {
if ch == '\n' {
newline_run += 1;
if newline_run <= 2 {
out.push(ch);
}
} else {
newline_run = 0;
out.push(ch);
}
}
out
}
fn ident_length(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() {
return None;
}
let first = bytes[0];
if !(first.is_ascii_alphabetic() || first == b'_' || first == b'$') {
return None;
}
let mut i = 1;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' || b == b'$' {
i += 1;
} else {
break;
}
}
Some(i)
}
fn parse_ts_call_from(text: &str, name: String) -> Result<(serde_json::Value, usize), String> {
let bytes = text.as_bytes();
let paren_open = name.len();
if bytes.get(paren_open) != Some(&b'(') {
return Err(format!(
"TOOL CALL PARSE ERROR: `{name}(` expected immediately after the tool name."
));
}
let mut p = TsValueParser::new(&text[paren_open + 1..]);
p.skip_ws_and_comments();
let args_value = if p.peek() == Some(b')') {
serde_json::Value::Object(serde_json::Map::new())
} else {
p.parse_value().map_err(|e| {
format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — {e}. \
Tool arguments must be a TypeScript object literal: `{{ key: value, key: value }}`."
)
})?
};
p.skip_ws_and_comments();
if p.peek() != Some(b')') {
return Err(format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — missing closing `)`. \
Every tool call must be a complete TypeScript expression."
));
}
let consumed_in_parser = p.position();
let total_consumed = paren_open + 1 + consumed_in_parser + 1;
match args_value {
serde_json::Value::Object(map) => Ok((serde_json::Value::Object(map), total_consumed)),
other => Err(format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — expected an object literal argument, \
got `{}`. Wrap the value in braces: `{name}({{ key: value }})`.",
other
)),
}
}
struct TsValueParser<'a> {
bytes: &'a [u8],
text: &'a str,
pos: usize,
}
impl<'a> TsValueParser<'a> {
fn new(text: &'a str) -> Self {
TsValueParser {
bytes: text.as_bytes(),
text,
pos: 0,
}
}
fn position(&self) -> usize {
self.pos
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn advance(&mut self) -> Option<u8> {
let b = self.peek()?;
self.pos += 1;
Some(b)
}
fn skip_ws_and_comments(&mut self) {
loop {
while let Some(b) = self.peek() {
if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
self.pos += 1;
} else {
break;
}
}
if self.peek() == Some(b'/') && self.bytes.get(self.pos + 1) == Some(&b'/') {
while let Some(b) = self.peek() {
if b == b'\n' {
self.pos += 1;
break;
}
self.pos += 1;
}
continue;
}
if self.peek() == Some(b'/') && self.bytes.get(self.pos + 1) == Some(&b'*') {
self.pos += 2;
while self.pos + 1 < self.bytes.len() {
if self.bytes[self.pos] == b'*' && self.bytes[self.pos + 1] == b'/' {
self.pos += 2;
break;
}
self.pos += 1;
}
continue;
}
break;
}
}
fn parse_value(&mut self) -> Result<serde_json::Value, String> {
self.skip_ws_and_comments();
let c = self.peek().ok_or("unexpected end of input")?;
match c {
b'{' => self.parse_object(),
b'[' => self.parse_array(),
b'"' | b'\'' => self.parse_string_literal(c),
b'`' => self.parse_template_literal(),
b't' | b'f' => self.parse_boolean(),
b'n' => self.parse_null(),
b'u' => self.parse_undefined(),
b'-' | b'0'..=b'9' => self.parse_number(),
other => Err(format!(
"unexpected character `{}` starting a value",
other as char
)),
}
}
fn parse_object(&mut self) -> Result<serde_json::Value, String> {
self.advance();
let mut map = serde_json::Map::new();
loop {
self.skip_ws_and_comments();
if self.peek() == Some(b'}') {
self.advance();
return Ok(serde_json::Value::Object(map));
}
let key = if let Some(b) = self.peek() {
if b == b'"' || b == b'\'' {
match self.parse_string_literal(b)? {
serde_json::Value::String(s) => s,
_ => unreachable!(),
}
} else {
let len = ident_length(&self.bytes[self.pos..])
.ok_or("expected an object key (identifier or string) inside `{ ... }`")?;
let k = self.text[self.pos..self.pos + len].to_string();
self.pos += len;
k
}
} else {
return Err("unexpected end of input inside object literal".to_string());
};
self.skip_ws_and_comments();
if self.peek() != Some(b':') {
return Err(format!(
"expected `:` after key `{key}` inside object literal"
));
}
self.advance();
self.skip_ws_and_comments();
let value = self.parse_value()?;
map.insert(key, value);
self.skip_ws_and_comments();
match self.peek() {
Some(b',') => {
self.advance();
continue;
}
Some(b'}') => {
self.advance();
return Ok(serde_json::Value::Object(map));
}
Some(other) => {
return Err(format!(
"expected `,` or `}}` after value inside object literal, got `{}`",
other as char
));
}
None => {
return Err("unexpected end of input inside object literal".to_string());
}
}
}
}
fn parse_array(&mut self) -> Result<serde_json::Value, String> {
self.advance(); let mut items = Vec::new();
loop {
self.skip_ws_and_comments();
if self.peek() == Some(b']') {
self.advance();
return Ok(serde_json::Value::Array(items));
}
items.push(self.parse_value()?);
self.skip_ws_and_comments();
match self.peek() {
Some(b',') => {
self.advance();
continue;
}
Some(b']') => {
self.advance();
return Ok(serde_json::Value::Array(items));
}
Some(other) => {
return Err(format!(
"expected `,` or `]` inside array literal, got `{}`",
other as char
));
}
None => {
return Err("unexpected end of input inside array literal".to_string());
}
}
}
}
fn parse_string_literal(&mut self, quote: u8) -> Result<serde_json::Value, String> {
self.advance(); let mut out = String::new();
loop {
match self.advance() {
None => return Err("unterminated string literal".to_string()),
Some(b) if b == quote => return Ok(serde_json::Value::String(out)),
Some(b'\\') => {
let esc = self
.advance()
.ok_or("unterminated escape sequence in string literal")?;
match esc {
b'n' => out.push('\n'),
b't' => out.push('\t'),
b'r' => out.push('\r'),
b'0' => out.push('\0'),
b'\\' => out.push('\\'),
b'\'' => out.push('\''),
b'"' => out.push('"'),
b'`' => out.push('`'),
b'\n' => { }
b'u' => {
let (ch, consumed) = parse_unicode_escape(&self.bytes[self.pos..])
.ok_or("invalid \\u escape in string literal")?;
out.push(ch);
self.pos += consumed;
}
b'x' => {
if self.pos + 2 > self.bytes.len() {
return Err("invalid \\x escape in string literal".to_string());
}
let hex = std::str::from_utf8(&self.bytes[self.pos..self.pos + 2])
.map_err(|_| "invalid \\x escape".to_string())?;
let code = u32::from_str_radix(hex, 16)
.map_err(|_| "invalid \\x escape".to_string())?;
if let Some(ch) = char::from_u32(code) {
out.push(ch);
self.pos += 2;
} else {
return Err("invalid \\x code point".to_string());
}
}
other => out.push(other as char),
}
}
Some(b) => {
out.push(b as char);
}
}
}
}
fn parse_template_literal(&mut self) -> Result<serde_json::Value, String> {
self.advance(); let mut out = String::new();
loop {
match self.advance() {
None => return Err("unterminated template literal".to_string()),
Some(b'`') => return Ok(serde_json::Value::String(out)),
Some(b'\\') => {
let esc = self
.advance()
.ok_or("unterminated escape in template literal")?;
match esc {
b'n' => out.push('\n'),
b't' => out.push('\t'),
b'r' => out.push('\r'),
b'\\' => out.push('\\'),
b'`' => out.push('`'),
b'$' => out.push('$'),
b'\n' => { }
other => {
out.push('\\');
out.push(other as char);
}
}
}
Some(b'$') if self.peek() == Some(b'{') => {
out.push('$');
out.push('{');
self.advance();
let mut depth = 1usize;
while depth > 0 {
match self.advance() {
None => {
return Err(
"unterminated ${{...}} interpolation in template literal"
.to_string(),
);
}
Some(b'{') => {
depth += 1;
out.push('{');
}
Some(b'}') => {
depth -= 1;
out.push('}');
}
Some(b) => out.push(b as char),
}
}
}
Some(b) => {
out.push(b as char);
}
}
}
}
fn parse_boolean(&mut self) -> Result<serde_json::Value, String> {
if self.text[self.pos..].starts_with("true") {
self.pos += 4;
Ok(serde_json::Value::Bool(true))
} else if self.text[self.pos..].starts_with("false") {
self.pos += 5;
Ok(serde_json::Value::Bool(false))
} else {
Err("expected `true` or `false`".to_string())
}
}
fn parse_null(&mut self) -> Result<serde_json::Value, String> {
if self.text[self.pos..].starts_with("null") {
self.pos += 4;
Ok(serde_json::Value::Null)
} else {
Err("expected `null`".to_string())
}
}
fn parse_undefined(&mut self) -> Result<serde_json::Value, String> {
if self.text[self.pos..].starts_with("undefined") {
self.pos += 9;
Ok(serde_json::Value::Null)
} else {
Err("expected `undefined`".to_string())
}
}
fn parse_number(&mut self) -> Result<serde_json::Value, String> {
let start = self.pos;
if self.peek() == Some(b'-') {
self.advance();
}
while let Some(b) = self.peek() {
if b.is_ascii_digit() || b == b'.' || b == b'e' || b == b'E' || b == b'+' || b == b'-' {
self.advance();
} else {
break;
}
}
let slice = &self.text[start..self.pos];
if let Ok(n) = slice.parse::<i64>() {
return Ok(serde_json::json!(n));
}
if let Ok(n) = slice.parse::<f64>() {
return serde_json::Number::from_f64(n)
.map(serde_json::Value::Number)
.ok_or_else(|| "non-finite number literal".to_string());
}
Err(format!("invalid number literal `{slice}`"))
}
}
fn parse_unicode_escape(bytes: &[u8]) -> Option<(char, usize)> {
if bytes.first() == Some(&b'{') {
let close = bytes.iter().position(|&b| b == b'}')?;
let hex = std::str::from_utf8(&bytes[1..close]).ok()?;
let code = u32::from_str_radix(hex, 16).ok()?;
Some((char::from_u32(code)?, close + 1))
} else if bytes.len() >= 4 {
let hex = std::str::from_utf8(&bytes[..4]).ok()?;
let code = u32::from_str_radix(hex, 16).ok()?;
Some((char::from_u32(code)?, 4))
} else {
None
}
}
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(d) => {
match d.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(|v| v.display()).unwrap_or_default();
let description = entry
.get("description")
.map(|v| v.display())
.unwrap_or_default();
let params = entry.get("parameters").and_then(|v| v.as_dict());
let output_schema = entry.get("outputSchema").map(vm_value_to_json);
let input_schema = vm_build_json_schema(params);
match provider {
"openai" | "openrouter" => {
let mut tool = serde_json::json!({
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": input_schema,
}
});
if let Some(output_schema) = output_schema.clone() {
tool["function"]["x-harn-output-schema"] = output_schema;
}
native_tools.push(tool);
}
_ => {
let mut tool = serde_json::json!({
"name": name,
"description": description,
"input_schema": input_schema,
});
if let Some(output_schema) = output_schema {
tool["x-harn-output-schema"] = output_schema;
}
native_tools.push(tool);
}
}
}
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)
}
fn vm_build_json_schema(params: Option<&BTreeMap<String, VmValue>>) -> serde_json::Value {
let mut properties = serde_json::Map::new();
let mut required = Vec::new();
if let Some(params) = params {
for (name, type_val) in params {
let type_str = type_val.display();
let json_type = match type_str.as_str() {
"int" | "integer" => "integer",
"float" | "number" => "number",
"bool" | "boolean" => "boolean",
"list" | "array" => "array",
"dict" | "object" => "object",
_ => "string",
};
properties.insert(name.clone(), serde_json::json!({"type": json_type}));
required.push(serde_json::Value::String(name.clone()));
}
}
serde_json::json!({
"type": "object",
"properties": properties,
"required": required,
"additionalProperties": false,
})
}
#[cfg(test)]
#[path = "tools_tests.rs"]
mod tests;