pub const AGENT_TOOL_HANDLER_RESULT_SCHEMA: &str = "harn.agent_tool_handler_result.v1";
pub(super) fn agent_tool_handler_result_text(value: &serde_json::Value) -> Option<&str> {
let object = value.as_object()?;
if object.get("schema")?.as_str()? != AGENT_TOOL_HANDLER_RESULT_SCHEMA {
return None;
}
object.get("text")?.as_str()
}
pub(super) fn carries_typed_outcome(
source: &crate::value::VmValue,
value: &serde_json::Value,
) -> bool {
source.struct_data().is_some()
&& value.as_object().is_some_and(|object| {
object.get("ok").is_some_and(serde_json::Value::is_boolean)
|| object
.get("success")
.is_some_and(serde_json::Value::is_boolean)
})
}
pub(super) fn harn_handler_result_value(val: &crate::value::VmValue) -> serde_json::Value {
let json = crate::llm::vm_value_to_json(val);
if agent_tool_handler_result_text(&json).is_some()
|| json_carries_screenshot(&json)
|| carries_typed_outcome(val, &json)
{
json
} else {
serde_json::Value::String(val.display())
}
}
fn json_carries_screenshot(value: &serde_json::Value) -> bool {
if crate::llm::content::is_screenshot_dict(value) {
return true;
}
match value {
serde_json::Value::Object(map) => map.values().any(json_carries_screenshot),
serde_json::Value::Array(items) => items.iter().any(json_carries_screenshot),
_ => false,
}
}
pub(super) fn coerce_and_classify_handler_result(
val: &crate::value::VmValue,
) -> (serde_json::Value, Option<&'static str>) {
let declared = super::ok_result_failure_category(&crate::llm::vm_value_to_json(val));
(harn_handler_result_value(val), declared)
}
#[cfg(test)]
mod tests {
use super::super::render_tool_result;
use super::harn_handler_result_value;
#[test]
fn a_plain_dict_handler_refusal_is_classified_before_it_is_rendered_away() {
let failure_shapes = [
serde_json::json!({"ok": false, "status": "blocked", "message": "apply blocked"}),
serde_json::json!({"ok": false, "error": "boom"}),
serde_json::json!({"success": false, "message": "rejected"}),
serde_json::json!({"isError": true, "message": "mcp shape"}),
serde_json::json!({"status": "error", "message": "nope"}),
];
for shape in failure_shapes {
let returned = crate::stdlib::json_to_vm_value(&shape);
let (payload, declared) = super::coerce_and_classify_handler_result(&returned);
assert_eq!(
declared,
Some("tool_error"),
"refusal must be classified before coercion: {shape:?}"
);
assert_eq!(
super::super::ok_result_failure_category(&payload),
None,
"the rendered payload is unparseable — that is the defect"
);
}
let ok_shape = serde_json::json!({"ok": true, "message": "fine"});
let (_, declared) =
super::coerce_and_classify_handler_result(&crate::stdlib::json_to_vm_value(&ok_shape));
assert_eq!(declared, None);
}
#[test]
fn carrying_the_outcome_does_not_change_the_rendered_payload() {
let shapes = [
serde_json::json!({"ok": false, "status": "blocked", "message": "apply blocked"}),
serde_json::json!({"ok": true, "message": "fine"}),
serde_json::json!({"success": false, "message": "rejected"}),
serde_json::json!({"isError": true, "message": "mcp shape"}),
serde_json::json!({"stdout": "done", "exit_code": 0}),
];
for shape in shapes {
let returned = crate::stdlib::json_to_vm_value(&shape);
let (payload, _) = super::coerce_and_classify_handler_result(&returned);
assert_eq!(
payload,
serde_json::Value::String(returned.display()),
"coerced payload must be the unchanged display string for {shape:?}"
);
}
}
#[test]
fn explicit_handler_result_preserves_data_and_renders_only_text() {
let envelope = serde_json::json!({
"schema": "harn.agent_tool_handler_result.v1",
"text": "human feedback",
"data": {"diagnostics_error_count": 2}
});
let value = crate::stdlib::json_to_vm_value(&envelope);
assert_eq!(harn_handler_result_value(&value), envelope);
assert_eq!(render_tool_result(&envelope), "human feedback");
}
#[test]
fn ordinary_handler_dict_keeps_legacy_display_rendering() {
let ordinary = serde_json::json!({"text": "human feedback", "data": {"count": 2}});
let value = crate::stdlib::json_to_vm_value(&ordinary);
let result = harn_handler_result_value(&value);
assert!(
result.is_string(),
"unmarked dict returns must keep their historical display-string payload"
);
}
#[test]
fn typed_domain_outcomes_remain_structured() {
let fields =
crate::value::DictMap::new().update("ok".into(), crate::value::VmValue::Bool(false));
let typed = crate::value::VmValue::struct_instance("ServiceError", fields);
assert_eq!(
harn_handler_result_value(&typed),
serde_json::json!({"ok": false})
);
let ordinary = crate::stdlib::json_to_vm_value(&serde_json::json!({"ok": false}));
assert!(
harn_handler_result_value(&ordinary).is_string(),
"plain dictionaries retain legacy display rendering"
);
}
}