vv-agent 0.4.7

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use serde_json::json;
use vv_agent::app_server::client::AppServerClient;
use vv_agent::app_server::protocol::{
    AppClientInfo, AppItemKind, ApprovalDecision, ApprovalResolveParams, JsonRpcMessage, RequestId,
    ServerRequest, ThreadReadParams, ThreadStartParams, TurnStartParams, UserInput,
};
use vv_agent::app_server::test_support::{
    approval_app_server_client, finish_response, scripted_app_server_client,
};

#[tokio::test]
async fn client_initializes_channel_server() {
    let mut client = scripted_app_server_client(vec![finish_response("hello")]);

    let initialized = client.initialize(client_info()).await.expect("initialize");

    assert_eq!(initialized.server_info.name, "vv-agent-rs");
    assert!(initialized.capabilities.thread);
    assert!(initialized.capabilities.event_replay);
}

#[tokio::test]
async fn client_starts_thread() {
    let mut client = initialized_client(vec![finish_response("hello")]).await;

    let response = client
        .start_thread(ThreadStartParams {
            cwd: None,
            title: Some("demo".to_string()),
            model: Some("demo-model".to_string()),
            ephemeral: false,
        })
        .await
        .expect("start thread");

    assert_eq!(response.thread.title.as_deref(), Some("demo"));
    assert_eq!(response.thread.model.as_deref(), Some("demo-model"));
}

#[tokio::test]
async fn client_starts_turn_and_collects_notifications() {
    let mut client = initialized_client(vec![finish_response("hello world")]).await;
    let thread = client
        .start_thread(ThreadStartParams {
            cwd: None,
            title: None,
            model: Some("demo-model".to_string()),
            ephemeral: false,
        })
        .await
        .expect("thread")
        .thread;

    let turn = client
        .start_turn(TurnStartParams {
            thread_id: thread.id.clone(),
            input: vec![UserInput {
                text: "say hello".to_string(),
            }],
            model: Some("demo-model".to_string()),
        })
        .await
        .expect("turn")
        .turn;
    assert_eq!(turn.thread_id, thread.id);

    let mut saw_delta = false;
    let mut saw_completed = false;
    for _ in 0..8 {
        match client.next_message().await.expect("message") {
            JsonRpcMessage::Notification(notification)
                if notification.method == "item/agentMessage/delta" =>
            {
                saw_delta = true;
            }
            JsonRpcMessage::Notification(notification)
                if notification.method == "turn/completed" =>
            {
                saw_completed = true;
                break;
            }
            _ => {}
        }
    }

    assert!(saw_delta);
    assert!(saw_completed);
}

#[tokio::test]
async fn client_resolves_approval_request() {
    let mut client = approval_app_server_client();
    client.initialize(client_info()).await.expect("initialize");
    let thread = client
        .start_thread(ThreadStartParams {
            cwd: None,
            title: None,
            model: Some("approval-model".to_string()),
            ephemeral: false,
        })
        .await
        .expect("thread")
        .thread;
    let turn = client
        .start_turn(TurnStartParams {
            thread_id: thread.id.clone(),
            input: vec![UserInput {
                text: "run approval tool".to_string(),
            }],
            model: Some("approval-model".to_string()),
        })
        .await
        .expect("turn")
        .turn;

    let approval_request = loop {
        let message = client.next_message().await.expect("message");
        if let JsonRpcMessage::Request(request) = message {
            let server_request: ServerRequest = serde_json::from_value(json!({
                "method": request.method,
                "params": request.params,
            }))
            .expect("server request");
            if matches!(server_request, ServerRequest::ApprovalRequest(_)) {
                break request;
            }
        }
    };
    let RequestId::String(request_id) = approval_request.id else {
        panic!("approval id should be string");
    };

    client
        .resolve_approval(ApprovalResolveParams {
            thread_id: thread.id,
            turn_id: turn.id,
            request_id,
            decision: ApprovalDecision::Allow,
        })
        .await
        .expect("resolve approval");

    let mut saw_resolved = false;
    let mut saw_completed = false;
    for _ in 0..8 {
        match client.next_message().await.expect("message") {
            JsonRpcMessage::Notification(notification)
                if notification.method == "approval/resolved" =>
            {
                saw_resolved = true;
            }
            JsonRpcMessage::Notification(notification)
                if notification.method == "turn/completed" =>
            {
                saw_completed = true;
                break;
            }
            _ => {}
        }
    }

    assert!(saw_resolved);
    assert!(saw_completed);
}

#[tokio::test]
async fn client_reads_thread_history() {
    let mut client = initialized_client(vec![finish_response("hello history")]).await;
    let thread = client
        .start_thread(ThreadStartParams {
            cwd: None,
            title: None,
            model: Some("demo-model".to_string()),
            ephemeral: false,
        })
        .await
        .expect("thread")
        .thread;
    client
        .start_turn(TurnStartParams {
            thread_id: thread.id.clone(),
            input: vec![UserInput {
                text: "write history".to_string(),
            }],
            model: Some("demo-model".to_string()),
        })
        .await
        .expect("turn");

    for _ in 0..8 {
        if matches!(
            client.next_message().await.expect("message"),
            JsonRpcMessage::Notification(notification) if notification.method == "turn/completed"
        ) {
            break;
        }
    }

    let history = client
        .read_thread(ThreadReadParams {
            thread_id: thread.id,
            after_item_id: None,
        })
        .await
        .expect("history");

    assert!(history
        .items
        .iter()
        .any(|item| item.kind == AppItemKind::AgentMessage));
}

async fn initialized_client(responses: Vec<vv_agent::LLMResponse>) -> AppServerClient {
    let mut client = scripted_app_server_client(responses);
    client.initialize(client_info()).await.expect("initialize");
    client
}

fn client_info() -> AppClientInfo {
    AppClientInfo {
        name: "test_client".to_string(),
        title: Some("Test Client".to_string()),
        version: Some("1.0.0".to_string()),
    }
}