harn-serve 0.10.166

Shared outbound workflow server core for Harn adapters
use super::*;

pub(super) async fn run_prompt_with_project_capability(
    request_tx: &mpsc::UnboundedSender<serde_json::Value>,
    response_rx: &mut mpsc::UnboundedReceiver<String>,
    session_id: &str,
    id: i64,
    prompt_text: &str,
    project_read_capability: bool,
) -> String {
    let message_id = format!("qualification-prompt-{id}");
    request_tx
        .send(serde_json::json!({
            "jsonrpc": "2.0",
            "id": id,
            "method": "session/prompt",
            "params": {
                "sessionId": session_id,
                "messageId": message_id,
                "prompt": [{"type": "text", "text": prompt_text}],
            },
        }))
        .expect("send session/prompt");

    let host_capabilities = if project_read_capability {
        serde_json::json!({"project": ["read_file"]})
    } else {
        serde_json::json!({})
    };
    let mut output = String::new();
    let mut saw_completed = false;
    let mut capability_frames = 0;
    let mut visible_frames = 0;
    for _ in 0..64 {
        let message = recv_json(response_rx).await;
        match message.get("method").and_then(|value| value.as_str()) {
            Some("host/capabilities") => {
                assert_eq!(message["params"]["sessionId"], session_id);
                assert_eq!(
                    message["params"]["promptCorrelation"]["messageId"],
                    message_id
                );
                capability_frames += 1;
                request_tx
                    .send(serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": message["id"].clone(),
                        "result": host_capabilities.clone(),
                    }))
                    .expect("send host/capabilities response");
            }
            Some("session/update")
                if message["params"]["update"]["sessionUpdate"] == "agent_message_chunk" =>
            {
                assert_eq!(message["params"]["sessionId"], session_id);
                assert_eq!(
                    message["params"]["promptCorrelation"]["messageId"], message_id,
                    "the canonical prompt must install its scoped output for each turn"
                );
                visible_frames += 1;
                let content = &message["params"]["update"]["content"];
                let text = content["text"].as_str().expect("chunk text");
                let visible_delta = content["_meta"]["harn"]["visible_delta"]
                    .as_str()
                    .expect("visible_delta");
                assert!(
                    !visible_delta.contains(if prompt_text == "one" { "two" } else { "one" }),
                    "each prompt turn gets a fresh bridge visible-text state"
                );
                output.push_str(text);
            }
            _ if message["id"] == id => {
                assert_eq!(message["result"]["stopReason"], "end_turn");
                assert!(
                    message.get("params").is_none(),
                    "responses keep their original shape"
                );
                saw_completed = true;
                break;
            }
            _ => {}
        }
    }
    assert!(saw_completed, "prompt {id} should complete successfully");
    assert!(
        capability_frames > 0,
        "prompt {id} must reach its host bridge"
    );
    assert!(
        visible_frames > 0,
        "prompt {id} must emit correlated visible output"
    );
    output
}

pub(super) async fn run_json_prompt(
    request_tx: &mpsc::UnboundedSender<serde_json::Value>,
    response_rx: &mut mpsc::UnboundedReceiver<String>,
    session_id: &str,
    id: i64,
    prompt_text: &str,
    expected_live_assistant: Option<&str>,
) -> serde_json::Value {
    request_tx
        .send(serde_json::json!({
            "jsonrpc": "2.0",
            "id": id,
            "method": "session/prompt",
            "params": {
                "sessionId": session_id,
                "prompt": [{"type": "text", "text": prompt_text}],
            },
        }))
        .expect("send session/prompt");

    let mut output = String::new();
    for _ in 0..64 {
        let message = recv_json(response_rx).await;
        match message.get("method").and_then(|value| value.as_str()) {
            Some("host/capabilities") => {
                request_tx
                    .send(serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": message["id"].clone(),
                        "result": {},
                    }))
                    .expect("send host/capabilities response");
            }
            Some("session/update")
                if message["params"]["update"]["sessionUpdate"] == "agent_message_chunk" =>
            {
                if let Some(text) = message["params"]["update"]["content"]["text"].as_str() {
                    output.push_str(text);
                }
            }
            _ if message["id"] == id => {
                assert_eq!(message["result"]["stopReason"], "end_turn");
                let json_output = if let Some(expected) = expected_live_assistant {
                    output.strip_prefix(expected).unwrap_or_else(|| {
                        panic!("missing live assistant prefix {expected:?}: {output:?}")
                    })
                } else {
                    &output
                };
                return serde_json::from_str(json_output.trim()).unwrap_or_else(|error| {
                    panic!("prompt JSON output after live projection: {error}; output={output:?}")
                });
            }
            _ => {}
        }
    }
    panic!("prompt {id} did not complete")
}