defect-cli 0.1.0-alpha.6

defect: a highly configurable, ACP-native, resource-frugal headless general-purpose agent CLI.
Documentation
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use agent_client_protocol::AcpAgent;
use agent_client_protocol_schema::{
    ContentBlock, InitializeRequest, NewSessionRequest, PromptRequest, ProtocolVersion,
    SessionNotification, SessionUpdate, StopReason,
};

#[tokio::test]
async fn stdio_echo_round_trip() {
    let state_root = tempfile::tempdir().expect("state tempdir");
    let config_root = tempfile::tempdir().expect("config tempdir");
    let process_cwd = tempfile::tempdir().expect("process cwd tempdir");
    let cwd = tempfile::tempdir().expect("cwd tempdir");
    let prompt_text = "stdio echo smoke";

    let binary = PathBuf::from(env!("CARGO_BIN_EXE_defect"));
    let agent = AcpAgent::from_args([
        format!("XDG_STATE_HOME={}", state_root.path().display()),
        format!("XDG_CONFIG_HOME={}", config_root.path().display()),
        "sh".to_string(),
        "-c".to_string(),
        r#"cd "$1" && shift && exec "$@""#.to_string(),
        "defect-stdio-smoke".to_string(),
        process_cwd.path().display().to_string(),
        binary.display().to_string(),
        "--provider".to_string(),
        "defect".to_string(),
    ])
    .expect("valid defect command");

    let updates: Arc<Mutex<Vec<SessionUpdate>>> = Arc::new(Mutex::new(Vec::new()));
    let updates_for_handler = Arc::clone(&updates);

    let stop_reason = agent_client_protocol::Client
        .builder()
        .name("stdio-smoke-client")
        .on_receive_notification(
            async move |notification: SessionNotification, _cx| {
                updates_for_handler
                    .lock()
                    .expect("updates mutex")
                    .push(notification.update);
                Ok(())
            },
            agent_client_protocol::on_receive_notification!(),
        )
        .connect_with(agent, async move |cx| {
            cx.send_request(InitializeRequest::new(ProtocolVersion::V1))
                .block_task()
                .await?;

            let session = cx
                .send_request(NewSessionRequest::new(cwd.path()))
                .block_task()
                .await?;

            let response = cx
                .send_request(PromptRequest::new(
                    session.session_id,
                    vec![ContentBlock::from(prompt_text)],
                ))
                .block_task()
                .await?;

            Ok(response.stop_reason)
        })
        .await
        .expect("client connection completed");

    assert_eq!(stop_reason, StopReason::EndTurn);

    let updates = updates.lock().expect("updates mutex");
    let assistant_chunks: String = updates
        .iter()
        .filter_map(|update| match update {
            SessionUpdate::AgentMessageChunk(chunk) => Some(&chunk.content),
            _ => None,
        })
        .filter_map(|content| match content {
            agent_client_protocol_schema::ContentBlock::Text(text) => Some(text.text.as_str()),
            _ => None,
        })
        .collect();

    assert!(
        assistant_chunks.contains(prompt_text),
        "session updates should contain echo text; got {assistant_chunks:?}",
    );
}