car-ffi-common 0.25.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrapper for `car-workflow::WorkflowEngine`.
//!
//! Lets JS / Python / WS callers run a multi-stage workflow definition
//! end-to-end. The workflow JSON shape comes from
//! [`car_workflow::Workflow`] (serde-derived); the result is
//! [`car_workflow::WorkflowResult`] serialized to JSON.
//!
//! Like `car_ffi_common::multi`, this takes an `AgentRunner` from the
//! caller — the FFI bindings supply their own (NAPI's
//! `StoredAgentRunner`, PyO3's `PyAgentRunner`) so the workflow's
//! agent stages dispatch through the registered JS/Python callback.

use car_multi::{AgentRunner, SharedInfra};
use car_workflow::{PausedWorkflow, Workflow, WorkflowEngine};
use std::collections::HashMap;
use std::sync::Arc;

/// Run a Workflow JSON definition to completion. Returns
/// [`WorkflowResult`](car_workflow::WorkflowResult) as JSON.
///
/// If the workflow hits a human-in-the-loop approval gate, the result has
/// `status == "paused"` and a `paused` checkpoint object. Hand that checkpoint
/// back to [`resume_workflow`] (with the human's response) to continue. The
/// caller owns checkpoint persistence — this function performs no I/O so the
/// engine stays pure; the server layer adds durable, exactly-once storage.
pub async fn run_workflow(
    workflow_json: &str,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let workflow: Workflow =
        serde_json::from_str(workflow_json).map_err(|e| format!("invalid workflow JSON: {}", e))?;
    let infra = SharedInfra::new();
    let engine = WorkflowEngine::new(runner, infra);
    let result = engine
        .run(&workflow)
        .await
        .map_err(|e| format!("workflow error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Resume a workflow parked at an approval gate.
///
/// `paused_json` is the `paused` checkpoint object returned by [`run_workflow`]
/// (or a prior `resume_workflow`); `input_json` is a JSON object of the human's
/// response fields. Returns the next [`WorkflowResult`](car_workflow::WorkflowResult)
/// as JSON — which may itself be `paused` again if the run hits another gate.
pub async fn resume_workflow(
    paused_json: &str,
    input_json: &str,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let paused: PausedWorkflow = serde_json::from_str(paused_json)
        .map_err(|e| format!("invalid paused checkpoint JSON: {}", e))?;
    let input: HashMap<String, serde_json::Value> = serde_json::from_str(input_json)
        .map_err(|e| format!("invalid approval input JSON: {}", e))?;
    let infra = SharedInfra::new();
    let engine = WorkflowEngine::new(runner, infra);
    let result = engine
        .resume(paused, input)
        .await
        .map_err(|e| format!("workflow resume error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Static analysis: validate a workflow definition without running it.
/// Returns the verification report as JSON.
///
/// Manually serialize the report because car_workflow::WorkflowVerifyResult
/// isn't Serde-derived (and shouldn't be — its Debug-only fields are
/// for human review, not API contract).
pub fn verify_workflow(workflow_json: &str) -> Result<String, String> {
    let workflow: Workflow =
        serde_json::from_str(workflow_json).map_err(|e| format!("invalid workflow JSON: {}", e))?;
    let report = car_workflow::verify_workflow(&workflow);
    let json = serde_json::json!({
        "valid": report.valid,
        "has_cycles": report.has_cycles,
        "reachable_stages": report.reachable_stages,
        "unreachable_stages": report.unreachable_stages,
        "issues": report.issues.iter().map(|i| format!("{:?}", i)).collect::<Vec<_>>(),
        // Advisory semantic findings (dangling edge-condition keys, unproduced
        // state dependencies). Non-blocking; do not affect `valid`.
        "semantic": car_workflow::semantic_issues(&workflow),
    });
    Ok(json.to_string())
}