car-ffi-common 0.22.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
Documentation
//! JSON wrappers for car-verify functions.

use car_ir::{ActionProposal, ToolSchema};
use serde_json::Value;
use std::collections::{HashMap, HashSet};

/// Verify a proposal from JSON inputs. Returns result JSON.
///
/// When `tool_schemas_json` is supplied (a JSON array of `ToolSchema`
/// objects, e.g. `[{"name":"echo","parameters":{...}}]`), each
/// `tool_call`'s parameters are validated against the matching
/// schema — catching type mismatches and missing required fields, not
/// just unknown tools (car-releases#56). `tool_names` remains
/// supported for existence-only checks when no schemas are at hand;
/// `tool_schemas_json` takes precedence when both are passed.
pub fn verify(
    proposal_json: &str,
    initial_state_json: Option<&str>,
    tool_names: Option<Vec<String>>,
    max_actions: usize,
    tool_schemas_json: Option<&str>,
) -> Result<String, String> {
    let proposal: ActionProposal =
        serde_json::from_str(proposal_json).map_err(|e| format!("invalid proposal JSON: {}", e))?;
    let initial_state: Option<HashMap<String, Value>> = initial_state_json
        .map(|s| serde_json::from_str(s))
        .transpose()
        .map_err(|e| format!("invalid state JSON: {}", e))?;

    let result = if let Some(schemas_json) = tool_schemas_json {
        let schemas: Vec<ToolSchema> = serde_json::from_str(schemas_json)
            .map_err(|e| format!("invalid tool schemas JSON: {}", e))?;
        let map: HashMap<String, ToolSchema> =
            schemas.into_iter().map(|s| (s.name.clone(), s)).collect();
        car_verify::verify_with_schemas(&proposal, initial_state.as_ref(), Some(&map), max_actions)
    } else {
        let tools: Option<HashSet<String>> = tool_names.map(|v| v.into_iter().collect());
        car_verify::verify(&proposal, initial_state.as_ref(), tools.as_ref(), max_actions)
    };

    serde_json::to_string(&serde_json::json!({
        "valid": result.valid,
        "issues": result.issues.iter().map(|i| serde_json::json!({
            "action_id": i.action_id,
            "severity": i.severity,
            "message": i.message,
        })).collect::<Vec<_>>(),
        "simulated_state": result.simulated_state,
        "execution_levels": result.execution_levels,
        "conflicts": result.conflicts,
    }))
    .map_err(|e| e.to_string())
}

/// Simulate a proposal from JSON inputs. Returns state JSON.
pub fn simulate(proposal_json: &str, initial_state_json: Option<&str>) -> Result<String, String> {
    let proposal: ActionProposal =
        serde_json::from_str(proposal_json).map_err(|e| format!("invalid proposal JSON: {}", e))?;
    let initial_state: Option<HashMap<String, Value>> = initial_state_json
        .map(|s| serde_json::from_str(s))
        .transpose()
        .map_err(|e| format!("invalid state JSON: {}", e))?;

    let state = car_verify::simulate(&proposal, initial_state.as_ref());
    serde_json::to_string(&state).map_err(|e| e.to_string())
}

/// Optimize a proposal from JSON. Returns optimized proposal JSON.
pub fn optimize(proposal_json: &str) -> Result<String, String> {
    let proposal: ActionProposal =
        serde_json::from_str(proposal_json).map_err(|e| format!("invalid proposal JSON: {}", e))?;
    let optimized = car_verify::optimize(&proposal);
    serde_json::to_string(&optimized).map_err(|e| e.to_string())
}

/// Check equivalence of two proposals from JSON.
pub fn equivalent(p1_json: &str, p2_json: &str) -> Result<bool, String> {
    let p1: ActionProposal =
        serde_json::from_str(p1_json).map_err(|e| format!("invalid proposal1 JSON: {}", e))?;
    let p2: ActionProposal =
        serde_json::from_str(p2_json).map_err(|e| format!("invalid proposal2 JSON: {}", e))?;
    Ok(car_verify::equivalent(&p1, &p2, None))
}