stynx-code 3.3.0

stynx-code — interactive AI coding assistant
use std::sync::Arc;
use std::sync::atomic::AtomicU8;

use stynx_code_engine::EngineEvent;
use stynx_code_errors::{AppError, AppResult};
use stynx_code_types::{
    ContentBlock, Conversation, Message, PermissionChecker, PermissionLevel, Role, SearchReadInfo, Tool,
};
use serde_json::{Value, json};

use super::conductor::{AgentCtx, AgentManager};

const WORKER_SYSTEM: &str = "\
You are a focused worker agent. Complete the assigned task efficiently \
and return a clear, concise, structured result. Use only the tools \
necessary. Do not over-explain.";

pub struct SpawnAgentTool {
    ctx: AgentCtx,
    manager: Arc<AgentManager>,
}

impl SpawnAgentTool {
    pub fn new(
        provider: Arc<dyn stynx_code_types::Provider>,
        registry: Arc<stynx_code_tools::ToolRegistry>,
        permission: Arc<dyn PermissionChecker>,
        mode: Arc<AtomicU8>,
        hooks: stynx_code_config::HooksConfig,
        manager: Arc<AgentManager>,
    ) -> Self {
        Self { ctx: AgentCtx { provider, registry, permission, mode, hooks }, manager }
    }
}

#[async_trait::async_trait]
impl Tool for SpawnAgentTool {
    fn name(&self) -> &str { "spawn_agent" }
    fn description(&self) -> &str {
        "Spawn a parallel worker agent to handle an independent subtask. \
         Returns immediately with an agent_id — the agent runs in the background. \
         Use wait_agent(agent_id) to collect the result. \
         Spawn multiple agents before waiting to maximise parallelism."
    }
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "name":   { "type": "string" },
                "task":   { "type": "string" },
                "system": { "type": "string" }
            },
            "required": ["name", "task"]
        })
    }
    fn permission_level(&self) -> PermissionLevel { PermissionLevel::Dangerous }
    async fn execute(&self, input: Value) -> AppResult<String> {
        let name = input["name"].as_str().unwrap_or("worker").to_string();
        let task = input["task"].as_str()
            .filter(|s| !s.is_empty())
            .ok_or_else(|| AppError::Tool("spawn_agent: 'task' is required".into()))?
            .to_string();
        let system = input["system"].as_str().unwrap_or(WORKER_SYSTEM).to_string();
        let (id, tx) = self.manager.register(name.clone(), task.clone());
        let ctx = self.ctx.clone();
        tokio::spawn(async move {
            let result = ctx.run(&system, &task).await.map_err(|e| e.to_string());
            let _ = tx.send(result);
        });
        Ok(json!({ "agent_id": id, "name": name, "status": "spawned" }).to_string())
    }
}

pub struct WaitAgentTool { manager: Arc<AgentManager> }

impl WaitAgentTool {
    pub fn new(manager: Arc<AgentManager>) -> Self { Self { manager } }
}

#[async_trait::async_trait]
impl Tool for WaitAgentTool {
    fn name(&self) -> &str { "wait_agent" }
    fn description(&self) -> &str {
        "Wait for a spawned agent to finish and return its output."
    }
    fn input_schema(&self) -> Value {
        json!({ "type": "object", "properties": { "agent_id": { "type": "string" } }, "required": ["agent_id"] })
    }
    fn permission_level(&self) -> PermissionLevel { PermissionLevel::ReadOnly }
    fn is_read_only(&self, _input: &Value) -> bool { true }
    async fn execute(&self, input: Value) -> AppResult<String> {
        let id = input["agent_id"].as_str()
            .ok_or_else(|| AppError::Tool("wait_agent: 'agent_id' is required".into()))?
            .to_string();
        match self.manager.wait_for(&id).await {
            Ok(output) => Ok(json!({ "agent_id": id, "status": "completed", "output": output }).to_string()),
            Err(e) => Ok(json!({ "agent_id": id, "status": "failed", "error": e }).to_string()),
        }
    }
}

pub struct ListAgentsTool { manager: Arc<AgentManager> }

impl ListAgentsTool {
    pub fn new(manager: Arc<AgentManager>) -> Self { Self { manager } }
}

#[async_trait::async_trait]
impl Tool for ListAgentsTool {
    fn name(&self) -> &str { "list_agents" }
    fn description(&self) -> &str {
        "List all spawned agents with their id, name, status, and elapsed seconds."
    }
    fn input_schema(&self) -> Value { json!({ "type": "object", "properties": {} }) }
    fn permission_level(&self) -> PermissionLevel { PermissionLevel::ReadOnly }
    fn is_read_only(&self, _input: &Value) -> bool { true }
    fn is_concurrent_safe(&self, _input: &Value) -> bool { true }
    fn is_search_or_read_command(&self, _input: &Value) -> SearchReadInfo {
        SearchReadInfo { is_search: false, is_read: false, is_list: true }
    }
    async fn execute(&self, _input: Value) -> AppResult<String> {
        let agents = self.manager.snapshot();
        Ok(serde_json::to_string(&agents).unwrap_or_else(|_| "[]".into()))
    }
}

impl AgentCtx {
    pub async fn run(&self, system: &str, task: &str) -> AppResult<String> {
        use std::sync::Mutex;
        let engine = stynx_code_engine::QueryEngine::new(
            self.provider.clone(), self.registry.clone(), self.permission.clone(), self.mode.clone(), self.hooks.clone(),
        );
        let mut conv = Conversation { system: Some(system.to_string()), ..Default::default() };
        conv.push(Message { role: Role::User, content: vec![ContentBlock::Text { text: task.to_string() }] });
        let output = Arc::new(Mutex::new(String::new()));
        let out_ref = output.clone();
        engine.run(conv, move |ev| {
            if let EngineEvent::TextDelta(t) = ev { out_ref.lock().unwrap().push_str(&t); }
        }).await?;
        Ok(output.lock().unwrap().clone())
    }
}