malvin 0.2.6

Non-interactive research and coding agent
use std::sync::atomic::{AtomicU64, Ordering};

use tokio::io::{AsyncBufReadExt, AsyncWriteExt};

use super::session::NpmPiSession;
use crate::acp::AgentError;

static SEQ: AtomicU64 = AtomicU64::new(1);

pub(crate) fn next_id() -> String {
    format!("npm-pi-{}", SEQ.fetch_add(1, Ordering::Relaxed))
}

pub(crate) async fn npm_pi_send_prompt(
    session: &NpmPiSession,
    prompt: &str,
) -> Result<(), AgentError> {
    let id = next_id();
    write_json(
        session,
        &serde_json::json!({
            "id": id,
            "type": "prompt",
            "message": prompt,
        }),
    )
    .await?;
    super::session_turn::consume_npm_pi_turn(session, &id).await
}

pub(crate) async fn write_json(
    session: &NpmPiSession,
    value: &serde_json::Value,
) -> Result<(), AgentError> {
    let mut stdin = session.stdin.lock().await;
    stdin
        .write_all(format!("{value}\n").as_bytes())
        .await
        .map_err(|e| AgentError::session_dead(format!("npm pi write: {e}")))?;
    stdin
        .flush()
        .await
        .map_err(|e| AgentError::session_dead(format!("npm pi flush: {e}")))
}

pub(crate) async fn read_json_waiting(
    session: &NpmPiSession,
    waiting_for: &str,
    turn: &mut crate::bridge_sdk::DrainIdleTurn,
) -> Result<serde_json::Value, AgentError> {
    let labels = crate::bridge_sdk::DrainIdleLabels {
        prefix: crate::model_id::ModelBackend::NpmPi.drain_idle_prefix(),
        waiting_for,
    };
    let health = Some(crate::bridge_sdk::DrainIdleHealthCtx {
        process_group_id: session.process_group_id,
        spawn_pid_baseline: &session.spawn_pid_baseline,
        tools_in_flight: false,
    });
    crate::bridge_sdk::await_next_with_idle_in_turn(labels, health, read_json_line(session), turn)
        .await
}

async fn read_json_line(session: &NpmPiSession) -> Result<serde_json::Value, AgentError> {
    let mut line = String::new();
    let n = {
        let mut out = session.stdout.lock().await;
        out.read_line(&mut line)
            .await
            .map_err(|e| AgentError::session_dead(format!("npm pi read: {e}")))?
    };
    if n == 0 {
        return Err(AgentError::session_dead("npm pi stdout closed"));
    }
    let line = line.trim_end_matches(['\r', '\n']);
    serde_json::from_str(line)
        .map_err(|e| AgentError::session_dead(format!("npm pi JSONL parse: {e}")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn next_id_increments() {
        let a = next_id();
        let b = next_id();
        assert_ne!(a, b);
        assert!(a.starts_with("npm-pi-"));
    }
}