stynx-code 3.12.1

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

use stynx_code_errors::{AppError, AppResult};
use stynx_code_types::{PermissionLevel, SearchReadInfo, Tool};
use serde_json::{Value, json};

use crate::infrastructure::intern_manager::{InternManager, InternStatus};

pub struct InternStatusTool {
    manager: Arc<InternManager>,
}

impl InternStatusTool {
    pub fn new(manager: Arc<InternManager>) -> Self { Self { manager } }
}

#[async_trait::async_trait]
impl Tool for InternStatusTool {
    fn name(&self) -> &str { "intern_status" }
    fn description(&self) -> &str {
        "Peek at a background intern delegation. Pass the handle returned by delegate_to_<intern> with background=true. \
         Returns {handle, intern, status, elapsed_s, last_action, task}. Status is one of: running, completed, failed, killed, timed_out. \
         Omit the handle to list all known interns."
    }
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "handle": { "type": "string", "description": "intern handle, e.g. intern-3. omit to list all." }
            }
        })
    }
    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> {
        if let Some(handle) = input.get("handle").and_then(|v| v.as_str()) {
            let snap = self.manager.status_snapshot(handle)
                .ok_or_else(|| AppError::Tool(format!("intern '{handle}' not found")))?;
            let status = match &snap.status {
                InternStatus::Running => "running".to_string(),
                InternStatus::Completed => "completed".to_string(),
                InternStatus::Failed(e) => format!("failed: {e}"),
                InternStatus::Killed => "killed".to_string(),
                InternStatus::TimedOut => "timed_out".to_string(),
            };
            Ok(json!({
                "handle": snap.id,
                "intern": snap.intern,
                "task": snap.task_preview,
                "status": status,
                "elapsed_s": snap.started.elapsed().as_secs(),
                "last_action": snap.last_action,
            }).to_string())
        } else {
            let snaps = self.manager.snapshot();
            Ok(serde_json::to_string(&snaps).unwrap_or_else(|_| "[]".into()))
        }
    }
}

pub struct InternKillTool { manager: Arc<InternManager> }

impl InternKillTool {
    pub fn new(manager: Arc<InternManager>) -> Self { Self { manager } }
}

#[async_trait::async_trait]
impl Tool for InternKillTool {
    fn name(&self) -> &str { "intern_kill" }
    fn description(&self) -> &str {
        "Abort a running background intern delegation. Use when the intern is stuck, looping, or going wild. \
         The task is dropped and any partial work is whatever the intern committed before the abort."
    }
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": { "handle": { "type": "string" } },
            "required": ["handle"]
        })
    }
    fn permission_level(&self) -> PermissionLevel { PermissionLevel::Dangerous }

    async fn execute(&self, input: Value) -> AppResult<String> {
        let handle = input.get("handle").and_then(|v| v.as_str())
            .ok_or_else(|| AppError::Tool("intern_kill: 'handle' is required".into()))?;
        let killed = self.manager.kill(handle);
        if killed {
            Ok(json!({"handle": handle, "status": "killed"}).to_string())
        } else {
            Ok(json!({"handle": handle, "status": "not_running", "note": "already finished, killed, or unknown handle"}).to_string())
        }
    }
}

pub struct InternWaitTool { manager: Arc<InternManager> }

impl InternWaitTool {
    pub fn new(manager: Arc<InternManager>) -> Self { Self { manager } }
}

#[async_trait::async_trait]
impl Tool for InternWaitTool {
    fn name(&self) -> &str { "intern_wait" }
    fn description(&self) -> &str {
        "Block until a background intern finishes and return its output. Optionally pass max_wait_secs (default 300) — \
         if the intern is still running after that, returns a timeout (the intern keeps running; call intern_kill to abort it). \
         Returns {handle, status, output} on success."
    }
    fn input_schema(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "handle": { "type": "string" },
                "max_wait_secs": { "type": "number" }
            },
            "required": ["handle"]
        })
    }
    fn permission_level(&self) -> PermissionLevel { PermissionLevel::ReadOnly }
    fn is_concurrent_safe(&self, _input: &Value) -> bool { true }

    async fn execute(&self, input: Value) -> AppResult<String> {
        let handle = input.get("handle").and_then(|v| v.as_str())
            .ok_or_else(|| AppError::Tool("intern_wait: 'handle' is required".into()))?
            .to_string();
        let max_wait_secs = input.get("max_wait_secs")
            .and_then(|v| v.as_u64())
            .unwrap_or(300);
        match self.manager.wait_for(&handle, max_wait_secs).await {
            Ok(output) => Ok(json!({"handle": handle, "status": "completed", "output": output}).to_string()),
            Err(e) if e.starts_with("wait timed out") => {
                Ok(json!({
                    "handle": handle,
                    "status": "still_running",
                    "note": format!("intern still running after {max_wait_secs}s — call intern_status, intern_wait again with a larger budget, or intern_kill to abort."),
                }).to_string())
            }
            Err(e) => Ok(json!({"handle": handle, "status": "failed", "error": e}).to_string()),
        }
    }
}