use serde_json::{Map, Value, json};
use crate::models::tool_call::FunctionCall;
pub(super) const TOOLSET_TYPE: &str = "computer_toolset_20260801";
pub(super) const TOOLSET_NAME: &str = "computer";
pub const TOOL: &str = "computer";
pub const HALTED: &str = "Not executed: an earlier computer action in this turn failed.";
pub(super) const REJECTION: &str = "computer_toolset";
pub(super) const REJECTION_NAMES: &[&str] = &[TOOLSET_TYPE, "computer_toolset"];
pub const MEMBERS: [&str; 17] = [
"screenshot",
"zoom",
"left_click",
"right_click",
"middle_click",
"double_click",
"triple_click",
"left_click_drag",
"mouse_move",
"left_mouse_down",
"left_mouse_up",
"cursor_position",
"scroll",
"type",
"key",
"hold_key",
"wait",
];
pub(super) fn declaration() -> Value {
json!({"type": TOOLSET_TYPE})
}
pub(super) fn canonicalize(member: &str, input: &Value) -> Option<FunctionCall> {
if !MEMBERS.contains(&member) {
return None;
}
let mut arguments = input.as_object().cloned().unwrap_or_default();
arguments.insert("action".to_string(), json!(member));
Some(FunctionCall {
name: TOOL.to_string(),
arguments: Value::Object(arguments),
})
}
pub(super) fn to_native(call: &FunctionCall) -> Option<(String, Value)> {
if call.name != TOOL {
return None;
}
let mut input: Map<String, Value> = call.arguments.as_object()?.clone();
let member = input.remove("action")?.as_str()?.to_string();
MEMBERS
.contains(&member.as_str())
.then_some((member, Value::Object(input)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_member_call_becomes_an_action_and_comes_back_as_written() {
for (member, input) in [
("screenshot", json!({})),
(
"left_click",
json!({"coordinate": [10, 20], "text": "shift"}),
),
("type", json!({"text": "hello"})),
("zoom", json!({"region": [0, 0, 100, 50]})),
("key", json!({"text": "ctrl+s", "repeat": 2})),
] {
let call = canonicalize(member, &input).expect(member);
assert_eq!(call.name, "computer");
assert_eq!(call.arguments["action"], member);
assert_eq!(to_native(&call), Some((member.to_string(), input)));
}
}
#[test]
fn a_name_that_is_no_member_is_left_alone() {
assert!(canonicalize("read_file", &json!({})).is_none());
let other = FunctionCall {
name: "computer".to_string(),
arguments: json!({"action": "teleport"}),
};
assert!(to_native(&other).is_none());
let not_computer = FunctionCall {
name: "read_file".to_string(),
arguments: json!({"action": "screenshot"}),
};
assert!(to_native(¬_computer).is_none());
}
}