procyon 0.3.0

Terminal development harness for Stellar and Soroban smart contracts, driven by a language model
use async_trait::async_trait;
use serde_json::{json, Value};

use super::Tool;
use crate::party::{self, PartyConfig};

/// Tool: party_mode
///
/// Runs a multi-agent discussion with multiple personas in parallel.
/// Each persona runs as an independent sub-agent and returns their perspective.
pub struct PartyModeTool;

#[async_trait]
impl Tool for PartyModeTool {
    fn name(&self) -> &str {
        "party_mode"
    }

    fn capability(&self) -> crate::risk::Capability {
        crate::risk::Capability::Delegating
    }

    fn description(&self) -> &str {
        "Run a multi-agent roundtable discussion. Multiple personas (Tyler, Elliot, Nicole, etc.) \
         respond in parallel, each from their own perspective. Use when the user wants multiple \
         opinions, a group discussion, or 'party mode'."
    }

    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "message": {
                    "type": "string",
                    "description": "The topic or question for the roundtable"
                },
                "personas": {
                    "type": "array",
                    "items": { "type": "string" },
                    "description": "Optional list of persona names/skills to include. If omitted, all available personas participate."
                },
                "model": {
                    "type": "string",
                    "description": "Optional model override for all sub-agents"
                }
            },
            "required": ["message"]
        })
    }

    async fn execute(&self, input: Value) -> Result<String, String> {
        let message = input
            .get("message")
            .and_then(|v| v.as_str())
            .ok_or_else(|| "Missing 'message' field".to_string())?
            .to_string();

        let personas = input.get("personas").and_then(|v| v.as_array()).map(|arr| {
            arr.iter()
                .filter_map(|v| v.as_str().map(|s| s.to_string()))
                .collect::<Vec<_>>()
        });

        let model = input
            .get("model")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let config = crate::config::AppConfig::load().unwrap_or_default();

        let party_config = PartyConfig {
            message,
            personas,
            model,
            timeout: None, // use default 120s
        };

        let responses = party::run_party(&config, party_config).await;

        Ok(party::format_responses(&responses))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tools::ToolRegistry;

    #[test]
    fn party_mode_tool_has_correct_name() {
        let tool = PartyModeTool;
        assert_eq!(tool.name(), "party_mode");
    }

    #[test]
    fn party_mode_schema_requires_message() {
        let tool = PartyModeTool;
        let schema = tool.input_schema();
        let required = schema["required"].as_array().unwrap();
        assert!(required.contains(&json!("message")));
    }

    #[test]
    fn party_mode_schema_has_optional_fields() {
        let tool = PartyModeTool;
        let schema = tool.input_schema();
        let props = schema["properties"].as_object().unwrap();
        assert!(props.contains_key("personas"));
        assert!(props.contains_key("model"));
    }

    #[tokio::test]
    async fn party_mode_rejects_missing_message() {
        let tool = PartyModeTool;
        let err = tool.execute(json!({})).await.unwrap_err();
        assert!(err.contains("Missing"), "got: {}", err);
    }

    #[tokio::test]
    async fn party_mode_tool_is_callable() {
        let tool = PartyModeTool;
        assert!(!tool.description().is_empty());
    }

    #[tokio::test]
    async fn party_mode_register_in_registry() {
        let mut registry = ToolRegistry::new();
        registry.register(Box::new(PartyModeTool));
        assert!(registry.get_tool("party_mode").is_some());
    }
}