use super::handle_local::coerce_integer_like_tool_args;
#[derive(Debug, serde::Serialize)]
struct ConversationToolCall {
id: String,
name: String,
arguments: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
provider_metadata: Option<serde_json::Value>,
}
impl ConversationToolCall {
fn from_result(value: &serde_json::Value) -> Self {
let id = value
.get("id")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let name = value
.get("name")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let arguments = value
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
let mut provider_metadata = value
.get("provider_metadata")
.filter(|metadata| metadata.is_object())
.cloned();
if let Some(signature) = crate::llm::providers::gemini_tool_call_thought_signature(value) {
let metadata = provider_metadata.get_or_insert_with(|| serde_json::json!({}));
metadata["gemini"]["thought_signature"] = serde_json::json!(signature);
}
Self {
id,
name,
arguments,
provider_metadata,
}
}
}
pub(crate) fn build_assistant_tool_message(
text: &str,
tool_calls: &[serde_json::Value],
) -> serde_json::Value {
let tool_calls = tool_calls
.iter()
.map(ConversationToolCall::from_result)
.collect::<Vec<_>>();
serde_json::json!({
"role": "assistant",
"content": text,
"tool_calls": tool_calls,
})
}
pub(crate) fn build_assistant_response_message(
text: &str,
blocks: &[serde_json::Value],
tool_calls: &[serde_json::Value],
reasoning: Option<&str>,
) -> serde_json::Value {
let portable_blocks = blocks
.iter()
.filter(|block| !crate::llm::reasoning_history::is_signed_anthropic_block(block))
.cloned()
.collect::<Vec<_>>();
let mut message = if !tool_calls.is_empty() {
build_assistant_tool_message(text, tool_calls)
} else if !portable_blocks.is_empty() {
serde_json::json!({
"role": "assistant",
"content": portable_blocks,
})
} else {
serde_json::json!({
"role": "assistant",
"content": text,
})
};
crate::llm::reasoning_history::attach_anthropic_continuation(&mut message, blocks);
if let Some(reasoning) = reasoning.filter(|value| !value.is_empty()) {
message["reasoning"] = serde_json::json!(reasoning);
}
message
}
pub(crate) fn normalize_tool_args(
name: &str,
args: serde_json::Value,
tools: Option<&crate::value::VmValue>,
) -> serde_json::Value {
let schema = super::collect_tool_schemas(tools, None)
.into_iter()
.find(|schema| schema.name == name);
let args = super::compat::unwrap_single_key_discriminator_envelope(args, schema.as_ref());
let mut obj = match args {
serde_json::Value::Object(obj) => obj,
other => return other,
};
if let Some(annotations) = crate::orchestration::current_tool_annotations(name) {
for (alias, canonical) in &annotations.arg_schema.arg_aliases {
if obj.contains_key(canonical) {
continue;
}
if let Some(value) = obj.remove(alias) {
obj.insert(canonical.clone(), value);
}
}
}
for value in obj.values_mut() {
strip_wrapping_heredoc_in_place(value);
}
let mut normalized = serde_json::Value::Object(obj);
coerce_integer_like_tool_args(&mut normalized);
super::compat::coerce_args_to_schema(normalized, schema.as_ref())
}
fn strip_wrapping_heredoc_in_place(value: &mut serde_json::Value) {
match value {
serde_json::Value::String(text) => {
if let Some(unwrapped) = crate::llm::tools::unwrap_fully_wrapping_heredoc(text) {
*text = unwrapped;
}
}
serde_json::Value::Array(items) => {
for item in items.iter_mut() {
strip_wrapping_heredoc_in_place(item);
}
}
serde_json::Value::Object(map) => {
for nested in map.values_mut() {
strip_wrapping_heredoc_in_place(nested);
}
}
_ => {}
}
}