car-ffi-common 0.24.0

Shared logic for FFI bindings (NAPI, PyO3) — JSON wrappers for verify, multi-agent, scheduler
//! Proxies for the daemon's `declagents.*` namespace (in-daemon declarative
//! agents). Thin [`DaemonClient`] calls.

use serde_json::{json, Value};

use crate::proxy::DaemonClient;

fn to_string(v: Value) -> Result<String, String> {
    serde_json::to_string(&v).map_err(|e| e.to_string())
}

/// `declagents.list` — registered declarative agents.
pub async fn list(client: &DaemonClient) -> Result<String, String> {
    client.call("declagents.list", json!({})).await.and_then(to_string)
}

/// `declagents.get` — one agent's spec.
pub async fn get(client: &DaemonClient, id: &str) -> Result<String, String> {
    client
        .call("declagents.get", json!({ "id": id }))
        .await
        .and_then(to_string)
}

/// `declagents.remove` — unregister an agent.
pub async fn remove(client: &DaemonClient, id: &str) -> Result<String, String> {
    client
        .call("declagents.remove", json!({ "id": id }))
        .await
        .and_then(to_string)
}

/// `declagents.set_enabled` — enable/disable an agent.
pub async fn set_enabled(client: &DaemonClient, id: &str, enabled: bool) -> Result<String, String> {
    client
        .call("declagents.set_enabled", json!({ "id": id, "enabled": enabled }))
        .await
        .and_then(to_string)
}

/// `declagents.invoke` — run an agent on an input, in-daemon. Returns
/// `{ output, turns, tool_calls, error? }`.
pub async fn invoke(client: &DaemonClient, id: &str, input: &str) -> Result<String, String> {
    client
        .call("declagents.invoke", json!({ "id": id, "input": input }))
        .await
        .and_then(to_string)
}

/// `declagents.route` — pick the agent whose capability best matches `need`
/// (capability-similarity routing). Returns `{ chosen, candidates, next_visited,
/// invoked, result? }`. With `invoke: true`, the top agent is run on `need` and
/// its `{ output, turns, tool_calls, error? }` lands in `result`.
///
/// This binding covers the network-entry case. Multi-hop Forward chaining (the
/// `from`/`visited` params) is WS-only — drive it over the raw JSON-RPC method.
pub async fn route(client: &DaemonClient, need: &str, invoke: bool) -> Result<String, String> {
    client
        .call("declagents.route", json!({ "need": need, "invoke": invoke }))
        .await
        .and_then(to_string)
}

/// `declagents.route_split` — decompose `need` into subtasks and route each to
/// its best-matching agent. Returns `{ subtasks: [{ subtask, chosen, score,
/// result? }], count, invoked }`. `max_subtasks` caps the split (clamped to
/// [1, 10]; None = default 5). With `invoke: true`, each subtask's chosen agent
/// runs on that subtask.
pub async fn route_split(
    client: &DaemonClient,
    need: &str,
    invoke: bool,
    max_subtasks: Option<u32>,
) -> Result<String, String> {
    let mut params = json!({ "need": need, "invoke": invoke });
    if let Some(m) = max_subtasks {
        params["max_subtasks"] = json!(m);
    }
    client.call("declagents.route_split", params).await.and_then(to_string)
}

/// `declagents.routing_stats` — read-only view of the learned routing topology.
/// Returns `{ agents: { id: { successes, failures, ema_success_rate } }, edges:
/// { from: { to: weight } } }`.
pub async fn routing_stats(client: &DaemonClient) -> Result<String, String> {
    client
        .call("declagents.routing_stats", json!({}))
        .await
        .and_then(to_string)
}