starweaver-cli 0.2.1

Command-line interface for Starweaver
Documentation
use std::collections::BTreeSet;

use starweaver_model::{ModelMessage, ModelRequestPart, ModelResponsePart, ToolReturnPart};
use starweaver_session::{ApprovalRecord, ApprovalStatus, DeferredToolRecord, ExecutionStatus};

use crate::CliError;

pub(super) fn existing_resume_tool_return_ids(history: &[ModelMessage]) -> BTreeSet<String> {
    let Some(last_tool_response_index) = history.iter().rposition(|message| match message {
        ModelMessage::Response(response) => response
            .parts
            .iter()
            .any(|part| matches!(part, ModelResponsePart::ToolCall(_))),
        ModelMessage::Request(_) => false,
    }) else {
        return BTreeSet::new();
    };
    history
        .iter()
        .skip(last_tool_response_index.saturating_add(1))
        .filter_map(|message| match message {
            ModelMessage::Request(request) => Some(&request.parts),
            ModelMessage::Response(_) => None,
        })
        .flat_map(|parts| parts.iter())
        .filter_map(|part| match part {
            ModelRequestPart::ToolReturn(tool_return) => Some(tool_return.tool_call_id.clone()),
            _ => None,
        })
        .collect()
}

pub(super) fn latest_tool_call_order(history: &[ModelMessage]) -> Vec<String> {
    history
        .iter()
        .rev()
        .find_map(|message| match message {
            ModelMessage::Response(response) => Some(
                response
                    .parts
                    .iter()
                    .filter_map(|part| match part {
                        ModelResponsePart::ToolCall(call) => Some(call.id.clone()),
                        _ => None,
                    })
                    .collect::<Vec<_>>(),
            ),
            ModelMessage::Request(_) => None,
        })
        .unwrap_or_default()
}

pub(super) fn tool_return_control_flow(tool_return: &ToolReturnPart) -> Option<&str> {
    tool_return
        .metadata
        .get("control_flow")
        .and_then(serde_json::Value::as_str)
}

pub(super) const fn deferred_status_is_unresolved(status: ExecutionStatus) -> bool {
    matches!(
        status,
        ExecutionStatus::Pending | ExecutionStatus::Running | ExecutionStatus::Waiting
    )
}

pub(super) fn pending_hitl_resume_error(
    run_id: &str,
    pending_approvals: &[String],
    pending_deferred: &[String],
) -> CliError {
    let mut details = Vec::new();
    if !pending_approvals.is_empty() {
        details.push(format!("approvals={}", pending_approvals.join(",")));
    }
    if !pending_deferred.is_empty() {
        details.push(format!("deferred_tools={}", pending_deferred.join(",")));
    }
    CliError::Usage(format!(
        "cannot resume run {run_id}: pending HITL decisions remain ({})",
        details.join("; ")
    ))
}

pub(super) fn approval_tool_return(record: &ApprovalRecord) -> Option<ToolReturnPart> {
    let decision = record.decision.as_ref();
    let mut metadata = serde_json::Map::new();
    metadata.insert(
        "control_flow_resolution".to_string(),
        serde_json::json!("approval"),
    );
    metadata.insert(
        "approval_id".to_string(),
        serde_json::json!(record.approval_id),
    );
    metadata.insert(
        "approval_status".to_string(),
        serde_json::json!(record.status),
    );
    if let Some(decision) = decision {
        metadata.insert("decision".to_string(), serde_json::json!(decision));
    }
    match record.status {
        ApprovalStatus::Approved => {
            let mut content = serde_json::json!({
                "approved": true,
                "approval_id": record.approval_id,
                "tool_name": record.action_name,
                "request": record.request,
            });
            if let Some(reason) = decision.and_then(|decision| decision.reason.as_ref()) {
                content["reason"] = serde_json::json!(reason);
            }
            Some(
                ToolReturnPart::new(
                    record.action_id.clone(),
                    record.action_name.clone(),
                    content,
                )
                .with_metadata(metadata),
            )
        }
        ApprovalStatus::Denied | ApprovalStatus::Expired | ApprovalStatus::Cancelled => {
            let reason = decision
                .and_then(|decision| decision.reason.clone())
                .unwrap_or_else(|| format!("approval {}", approval_status_name(record.status)));
            Some(
                ToolReturnPart::new(
                    record.action_id.clone(),
                    record.action_name.clone(),
                    serde_json::json!({
                        "approved": false,
                        "approval_id": record.approval_id,
                        "tool_name": record.action_name,
                        "reason": reason,
                    }),
                )
                .with_error(true)
                .with_metadata(metadata),
            )
        }
        ApprovalStatus::Pending => None,
    }
}

pub(super) fn deferred_tool_return(record: &DeferredToolRecord) -> Option<ToolReturnPart> {
    let mut metadata = serde_json::Map::new();
    metadata.insert(
        "control_flow_resolution".to_string(),
        serde_json::json!("deferred"),
    );
    metadata.insert(
        "deferred_id".to_string(),
        serde_json::json!(record.deferred_id),
    );
    metadata.insert(
        "deferred_status".to_string(),
        serde_json::json!(record.status),
    );
    match record.status {
        ExecutionStatus::Completed => Some(
            ToolReturnPart::new(
                record.tool_call_id.clone(),
                record.tool_name.clone(),
                record.response.clone(),
            )
            .with_metadata(metadata),
        ),
        ExecutionStatus::Failed | ExecutionStatus::Cancelled => Some(
            ToolReturnPart::new(
                record.tool_call_id.clone(),
                record.tool_name.clone(),
                if record.response.is_null() {
                    serde_json::json!({"error": deferred_status_name(record.status)})
                } else {
                    record.response.clone()
                },
            )
            .with_error(true)
            .with_metadata(metadata),
        ),
        ExecutionStatus::Pending | ExecutionStatus::Running | ExecutionStatus::Waiting => None,
    }
}

const fn approval_status_name(status: ApprovalStatus) -> &'static str {
    match status {
        ApprovalStatus::Pending => "pending",
        ApprovalStatus::Approved => "approved",
        ApprovalStatus::Denied => "denied",
        ApprovalStatus::Expired => "expired",
        ApprovalStatus::Cancelled => "cancelled",
    }
}

const fn deferred_status_name(status: ExecutionStatus) -> &'static str {
    match status {
        ExecutionStatus::Pending => "pending",
        ExecutionStatus::Running => "running",
        ExecutionStatus::Waiting => "waiting",
        ExecutionStatus::Completed => "completed",
        ExecutionStatus::Failed => "failed",
        ExecutionStatus::Cancelled => "cancelled",
    }
}