car-ffi-common 0.24.1

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! JSON wrappers for car-multi pattern runners.
//!
//! Each function takes JSON strings for specs, an AgentRunner, and returns result JSON.
//! The caller provides the AgentRunner impl (NAPI or PyO3 specific).

use car_multi::{AgentRunner, AgentSpec, BudgetLimits, SharedInfra, SwarmMode};
use std::sync::Arc;

/// Build a [`SharedInfra`], attaching a coordination budget when `budget_json`
/// is supplied. `budget_json` is a [`BudgetLimits`] object, e.g.
/// `{"max_total_tokens": 200000, "max_agents": 12, "max_cost_usd": 5.0}`.
/// Any omitted field is unbounded.
fn infra_with_budget(budget_json: Option<&str>) -> Result<SharedInfra, String> {
    let infra = SharedInfra::new();
    match budget_json {
        None => Ok(infra),
        Some(json) => {
            let limits: BudgetLimits =
                serde_json::from_str(json).map_err(|e| format!("invalid budget JSON: {}", e))?;
            Ok(infra.with_budget(limits))
        }
    }
}

/// Run a Swarm pattern from JSON inputs.
pub async fn run_swarm(
    mode: &str,
    agents_json: &str,
    task: &str,
    synthesizer_json: Option<&str>,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let agent_specs: Vec<AgentSpec> =
        serde_json::from_str(agents_json).map_err(|e| format!("invalid agents JSON: {}", e))?;
    let swarm_mode: SwarmMode = serde_json::from_str(&format!("\"{}\"", mode))
        .map_err(|e| format!("invalid mode '{}': {}", mode, e))?;
    let synth: Option<AgentSpec> = synthesizer_json
        .map(|s| serde_json::from_str(s))
        .transpose()
        .map_err(|e| format!("invalid synthesizer JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let mut swarm = car_multi::Swarm::new(agent_specs, swarm_mode);
    if let Some(s) = synth {
        swarm = swarm.with_synthesizer(s);
    }

    let result = swarm
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("swarm error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run a Pipeline pattern from JSON inputs.
pub async fn run_pipeline(
    stages_json: &str,
    task: &str,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let stage_specs: Vec<AgentSpec> =
        serde_json::from_str(stages_json).map_err(|e| format!("invalid stages JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let result = car_multi::Pipeline::new(stage_specs)
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("pipeline error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run a Supervisor pattern from JSON inputs.
pub async fn run_supervisor(
    workers_json: &str,
    supervisor_json: &str,
    task: &str,
    max_rounds: u32,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let worker_specs: Vec<AgentSpec> =
        serde_json::from_str(workers_json).map_err(|e| format!("invalid workers JSON: {}", e))?;
    let supervisor_spec: AgentSpec = serde_json::from_str(supervisor_json)
        .map_err(|e| format!("invalid supervisor JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let result = car_multi::Supervisor::new(worker_specs, supervisor_spec)
        .with_max_rounds(max_rounds)
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("supervisor error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run a MapReduce pattern from JSON inputs.
pub async fn run_map_reduce(
    mapper_json: &str,
    reducer_json: &str,
    task: &str,
    items_json: &str,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let mapper_spec: AgentSpec =
        serde_json::from_str(mapper_json).map_err(|e| format!("invalid mapper JSON: {}", e))?;
    let reducer_spec: AgentSpec =
        serde_json::from_str(reducer_json).map_err(|e| format!("invalid reducer JSON: {}", e))?;
    let item_list: Vec<String> =
        serde_json::from_str(items_json).map_err(|e| format!("invalid items JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let result = car_multi::MapReduce::new(mapper_spec, reducer_spec)
        .run(task, &item_list, &runner, &infra)
        .await
        .map_err(|e| format!("map_reduce error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run a Vote pattern from JSON inputs.
pub async fn run_vote(
    agents_json: &str,
    task: &str,
    synthesizer_json: Option<&str>,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let agent_specs: Vec<AgentSpec> =
        serde_json::from_str(agents_json).map_err(|e| format!("invalid agents JSON: {}", e))?;
    let synth: Option<AgentSpec> = synthesizer_json
        .map(|s| serde_json::from_str(s))
        .transpose()
        .map_err(|e| format!("invalid synthesizer JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let mut vote = car_multi::Vote::new(agent_specs);
    if let Some(s) = synth {
        vote = vote.with_synthesizer(s);
    }

    let result = vote
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("vote error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run a Tournament pattern from JSON inputs: rank `competitors` by
/// single-elimination pairwise judging with a `judge` agent.
pub async fn run_tournament(
    competitors_json: &str,
    judge_json: &str,
    task: &str,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let competitors: Vec<AgentSpec> = serde_json::from_str(competitors_json)
        .map_err(|e| format!("invalid competitors JSON: {}", e))?;
    let judge: AgentSpec =
        serde_json::from_str(judge_json).map_err(|e| format!("invalid judge JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let result = car_multi::Tournament::new(competitors, judge)
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("tournament error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}

/// Run an agent that can spawn isolated, tool-constrained sub-agents.
///
/// `main_json` is the main [`AgentSpec`]; its `tools` define the universe a
/// spawned sub-agent may draw from. During the run the main agent can call the
/// `spawn_subtask` tool to hand a *subset* of those tools to an ephemeral
/// sub-agent — the subset is enforced by the tool schema's `enum` (verified by
/// the runtime validator) and re-checked at execution. Returns
/// [`SpawnSubtaskResult`](car_multi::SpawnSubtaskResult) as JSON.
pub async fn run_subtask(
    main_json: &str,
    task: &str,
    budget_json: Option<&str>,
    runner: Arc<dyn AgentRunner>,
) -> Result<String, String> {
    let main_spec: AgentSpec =
        serde_json::from_str(main_json).map_err(|e| format!("invalid main agent JSON: {}", e))?;

    let infra = infra_with_budget(budget_json)?;
    let result = car_multi::SpawnSubtask::new(main_spec)
        .run(task, &runner, &infra)
        .await
        .map_err(|e| format!("spawn_subtask error: {}", e))?;
    serde_json::to_string(&result).map_err(|e| e.to_string())
}