unified-agent-api 0.3.4

Agent-agnostic facade and registry for wrapper backends
Documentation
use crate::{AgentWrapperEvent, AgentWrapperEventKind, AgentWrapperKind};

use super::{
    AGENT_KIND, CAP_SESSION_HANDLE_V1, CAP_TOOLS_STRUCTURED_V1, CHANNEL_ASSISTANT, CHANNEL_TOOL,
};

pub(super) fn map_stream_json_event(
    ev: claude_code::ClaudeStreamJsonEvent,
) -> Vec<AgentWrapperEvent> {
    match ev {
        claude_code::ClaudeStreamJsonEvent::SystemInit { .. } => {
            vec![status_event(Some("system init".to_string()))]
        }
        claude_code::ClaudeStreamJsonEvent::SystemOther { subtype, .. } => {
            vec![status_event(Some(format!("system {subtype}")))]
        }
        claude_code::ClaudeStreamJsonEvent::ResultError { .. } => {
            vec![error_event("result error".to_string())]
        }
        claude_code::ClaudeStreamJsonEvent::ResultSuccess { .. } => {
            vec![status_event(Some("result success".to_string()))]
        }
        claude_code::ClaudeStreamJsonEvent::AssistantMessage { raw, .. } => {
            map_assistant_message(&raw)
        }
        claude_code::ClaudeStreamJsonEvent::StreamEvent { stream, .. } => {
            map_stream_event(&stream.raw)
        }
        claude_code::ClaudeStreamJsonEvent::UserMessage { .. } => vec![status_event(None)],
        claude_code::ClaudeStreamJsonEvent::Unknown { .. } => vec![unknown_event()],
    }
}

pub(super) fn extract_assistant_message_final_text(raw: &serde_json::Value) -> Option<String> {
    let blocks = raw
        .get("message")
        .and_then(|m| m.get("content"))
        .and_then(|c| c.as_array())?;

    let mut texts = Vec::new();
    for block in blocks {
        let Some(obj) = block.as_object() else {
            continue;
        };
        if obj.get("type").and_then(|v| v.as_str()) != Some("text") {
            continue;
        }
        let Some(text) = obj.get("text").and_then(|v| v.as_str()) else {
            continue;
        };
        texts.push(text);
    }

    if texts.is_empty() {
        None
    } else {
        Some(texts.join("\n"))
    }
}

pub(super) fn map_assistant_message(raw: &serde_json::Value) -> Vec<AgentWrapperEvent> {
    let Some(blocks) = raw
        .get("message")
        .and_then(|m| m.get("content"))
        .and_then(|c| c.as_array())
    else {
        return vec![unknown_event()];
    };

    let mut out = Vec::new();
    for block in blocks {
        let Some(obj) = block.as_object() else {
            out.push(unknown_event());
            continue;
        };
        let block_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
        match block_type {
            "text" => {
                if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
                    out.extend(text_output_events(text, Some(CHANNEL_ASSISTANT)));
                } else {
                    out.push(unknown_event());
                }
            }
            "tool_use" => {
                let tool_name = obj
                    .get("name")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string());
                let tool_use_id = obj
                    .get("id")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string());
                out.push(tool_call_start_event(tool_name, tool_use_id));
            }
            "tool_result" => {
                let tool_use_id = obj
                    .get("tool_use_id")
                    .and_then(|v| v.as_str())
                    .map(|v| v.to_string());
                out.push(tool_result_complete_event(tool_use_id));
            }
            _ => out.push(unknown_event()),
        }
    }
    out
}

pub(super) fn map_stream_event(raw: &serde_json::Value) -> Vec<AgentWrapperEvent> {
    let Some(obj) = raw.as_object() else {
        return vec![unknown_event()];
    };
    let event_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
    match event_type {
        "content_block_start" => {
            let Some(content_block) = obj.get("content_block").and_then(|v| v.as_object()) else {
                return vec![unknown_event()];
            };
            let block_type = content_block
                .get("type")
                .and_then(|v| v.as_str())
                .unwrap_or("");
            match block_type {
                "tool_use" => {
                    let tool_name = content_block
                        .get("name")
                        .and_then(|v| v.as_str())
                        .map(|v| v.to_string());
                    let tool_use_id = content_block
                        .get("id")
                        .and_then(|v| v.as_str())
                        .map(|v| v.to_string());
                    vec![tool_call_start_event(tool_name, tool_use_id)]
                }
                "tool_result" => {
                    let tool_use_id = content_block
                        .get("tool_use_id")
                        .and_then(|v| v.as_str())
                        .map(|v| v.to_string());
                    vec![tool_result_complete_event(tool_use_id)]
                }
                _ => vec![unknown_event()],
            }
        }
        "content_block_delta" => {
            let Some(delta) = obj.get("delta").and_then(|v| v.as_object()) else {
                return vec![unknown_event()];
            };
            let delta_type = delta.get("type").and_then(|v| v.as_str()).unwrap_or("");
            match delta_type {
                "text_delta" => {
                    let Some(text) = delta.get("text").and_then(|v| v.as_str()) else {
                        return vec![unknown_event()];
                    };
                    text_output_events(text, Some(CHANNEL_ASSISTANT))
                }
                "input_json_delta" => vec![tool_call_delta_event()],
                _ => vec![unknown_event()],
            }
        }
        _ => vec![unknown_event()],
    }
}

fn tool_call_start_event(
    tool_name: Option<String>,
    tool_use_id: Option<String>,
) -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::ToolCall,
        channel: Some(CHANNEL_TOOL.to_string()),
        text: None,
        message: None,
        data: Some(tool_facet(
            "tool_use",
            "start",
            "running",
            tool_name,
            tool_use_id,
        )),
    }
}

fn tool_call_delta_event() -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::ToolCall,
        channel: Some(CHANNEL_TOOL.to_string()),
        text: None,
        message: None,
        data: Some(tool_facet("tool_use", "delta", "running", None, None)),
    }
}

fn tool_result_complete_event(tool_use_id: Option<String>) -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::ToolResult,
        channel: Some(CHANNEL_TOOL.to_string()),
        text: None,
        message: None,
        data: Some(tool_facet(
            "tool_result",
            "complete",
            "completed",
            None,
            tool_use_id,
        )),
    }
}

fn tool_facet(
    kind: &'static str,
    phase: &'static str,
    status: &'static str,
    tool_name: Option<String>,
    tool_use_id: Option<String>,
) -> serde_json::Value {
    serde_json::json!({
        "schema": CAP_TOOLS_STRUCTURED_V1,
        "tool": {
            "backend_item_id": null,
            "thread_id": null,
            "turn_id": null,
            "kind": kind,
            "phase": phase,
            "status": status,
            "exit_code": null,
            "bytes": { "stdout": 0, "stderr": 0, "diff": 0, "result": 0 },
            "tool_name": tool_name,
            "tool_use_id": tool_use_id,
        },
    })
}

pub(super) fn session_handle_facet(session_id: &str) -> serde_json::Value {
    serde_json::json!({
        "schema": CAP_SESSION_HANDLE_V1,
        "session": { "id": session_id },
    })
}

pub(super) fn status_event(message: Option<String>) -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::Status,
        channel: Some("status".to_string()),
        text: None,
        message,
        data: None,
    }
}

pub(super) fn error_event(message: String) -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::Error,
        channel: Some("error".to_string()),
        text: None,
        message: Some(message),
        data: None,
    }
}

fn unknown_event() -> AgentWrapperEvent {
    AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::Unknown,
        channel: None,
        text: None,
        message: None,
        data: None,
    }
}

fn text_output_events(text: &str, channel: Option<&str>) -> Vec<AgentWrapperEvent> {
    vec![AgentWrapperEvent {
        agent_kind: AgentWrapperKind(AGENT_KIND.to_string()),
        kind: AgentWrapperEventKind::TextOutput,
        channel: channel.map(|c| c.to_string()),
        text: Some(text.to_string()),
        message: None,
        data: None,
    }]
}