use super::{dict_get, list_items, vm_to_json};
use crate::value::{VmDictExt, VmValue};
pub(super) fn tool_result_message_for_provider(
provider: &str,
model: &str,
tool_format: &str,
name: &str,
tool_call_id: &str,
observation: &str,
screenshots: &[VmValue],
) -> VmValue {
let mut msg = crate::value::DictMap::new();
let is_text_channel = matches!(
crate::llm_config::tool_format_channel(tool_format),
Some(crate::llm_config::ToolFormatChannel::Text)
);
if is_text_channel {
msg.put_str("role", "user");
} else if crate::llm::provider::provider_uses_anthropic_messages(provider, model) {
msg.put_str("role", "tool_result");
msg.put_str("tool_use_id", tool_call_id);
} else {
msg.put_str("role", "tool");
msg.put_str("name", name);
if !tool_call_id.is_empty() {
msg.put_str("tool_call_id", tool_call_id);
}
}
if screenshots.is_empty() {
msg.put_str("content", observation);
} else {
let summary = screenshot_result_summary(observation);
let mut text_block = crate::value::DictMap::new();
text_block.put_str("type", "text");
text_block.put_str("text", &summary);
let mut content = Vec::with_capacity(1 + screenshots.len());
content.push(VmValue::dict(text_block));
content.extend(screenshots.iter().cloned());
msg.put("content", VmValue::List(std::sync::Arc::new(content)));
}
VmValue::dict(msg)
}
fn screenshot_result_summary(observation: &str) -> String {
if let Some(idx) = observation.find("text: ") {
let rest = &observation[idx + "text: ".len()..];
let end = rest.find(", screenshot:").unwrap_or(rest.len());
let candidate = rest[..end].trim();
if !candidate.is_empty() && candidate.len() < 400 {
return format!("{candidate} (screenshot attached below)");
}
}
"Screenshot captured (attached below).".to_string()
}
pub(super) fn screenshots_from_tool_result(result: &VmValue) -> Vec<VmValue> {
fn collect(value: &serde_json::Value, out: &mut Vec<serde_json::Value>) {
if crate::llm::content::is_screenshot_dict(value) {
out.push(value.clone());
return;
}
match value {
serde_json::Value::Object(map) => {
for nested in map.values() {
collect(nested, out);
}
}
serde_json::Value::Array(items) => {
for item in items {
collect(item, out);
}
}
_ => {}
}
}
let json = vm_to_json(result);
let mut found = Vec::new();
collect(&json, &mut found);
found.iter().map(crate::stdlib::json_to_vm_value).collect()
}
pub(super) struct AssistantToolUse {
id: String,
name: String,
}
pub(super) fn assistant_tool_use_blocks(message: &VmValue) -> Vec<AssistantToolUse> {
let mut blocks = Vec::new();
for call in list_items(
&dict_get(message, "tool_calls")
.cloned()
.unwrap_or(VmValue::Nil),
) {
let id = dict_get(&call, "id")
.map(|v| v.display())
.unwrap_or_default();
let name = dict_get(&call, "name")
.map(|v| v.display())
.or_else(|| {
dict_get(&call, "function").and_then(|f| dict_get(f, "name").map(|v| v.display()))
})
.unwrap_or_default();
blocks.push(AssistantToolUse { id, name });
}
if let Some(content) = dict_get(message, "content") {
for block in list_items(content) {
let block_type = dict_get(&block, "type")
.map(|v| v.display())
.unwrap_or_default();
if block_type == "tool_use" {
let id = dict_get(&block, "id")
.map(|v| v.display())
.unwrap_or_default();
let name = dict_get(&block, "name")
.map(|v| v.display())
.unwrap_or_default();
blocks.push(AssistantToolUse { id, name });
continue;
}
if let Some(function_call) = dict_get(&block, "functionCall") {
let id = dict_get(function_call, "id")
.map(|v| v.display())
.unwrap_or_default();
let name = dict_get(function_call, "name")
.map(|v| v.display())
.unwrap_or_default();
blocks.push(AssistantToolUse { id, name });
}
}
}
blocks
}
pub(super) fn paired_tool_result_ids(messages: &[VmValue]) -> std::collections::BTreeSet<String> {
let mut ids = std::collections::BTreeSet::new();
for message in messages {
let role = dict_get(message, "role")
.map(|v| v.display())
.unwrap_or_default();
if role != "tool_result" && role != "tool" {
continue;
}
let id = dict_get(message, "tool_use_id")
.or_else(|| dict_get(message, "tool_call_id"))
.map(|v| v.display())
.unwrap_or_default();
if !id.is_empty() {
ids.insert(id);
}
}
ids
}
pub(super) fn synthesize_orphan_tool_results(
last_assistant: &VmValue,
provider: &str,
model: &str,
feedback: &str,
already_paired: &std::collections::BTreeSet<String>,
) -> Vec<VmValue> {
let mut out = Vec::new();
for block in assistant_tool_use_blocks(last_assistant) {
if !block.id.is_empty() && already_paired.contains(&block.id) {
continue;
}
out.push(tool_result_message_for_provider(
provider,
model,
"native",
&block.name,
&block.id,
feedback,
&[],
));
}
out
}