nanocodex-tools 0.1.0

Code Mode and heterogeneous tool runtime for Nanocodex
Documentation
use nanocodex_core::ToolDefinition;
use serde::Deserialize;
use serde_json::json;
use tokio::sync::Mutex;

use super::{Tool, ToolContext, ToolExecution, ToolInput, ToolResult};

pub(super) struct PlanHandler {
    current: Mutex<Option<UpdatePlanArgs>>,
}

impl PlanHandler {
    pub(super) const fn new() -> Self {
        Self {
            current: Mutex::const_new(None),
        }
    }
}

#[async_trait::async_trait]
impl Tool for PlanHandler {
    fn name(&self) -> &'static str {
        "update_plan"
    }

    fn definition(&self) -> ToolDefinition {
        ToolDefinition::function(
            self.name(),
            "Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n",
            json!({
                "type": "object",
                "properties": {
                    "explanation": {
                        "type": "string",
                        "description": "Optional explanation for this plan update."
                    },
                    "plan": {
                        "type": "array",
                        "description": "The list of steps",
                        "items": {
                            "type": "object",
                            "properties": {
                                "step": { "type": "string", "description": "Task step text." },
                                "status": {
                                    "type": "string",
                                    "enum": ["pending", "in_progress", "completed"],
                                    "description": "Step status."
                                }
                            },
                            "required": ["step", "status"],
                            "additionalProperties": false
                        }
                    }
                },
                "required": ["plan"],
                "additionalProperties": false
            }),
        )
    }

    async fn execute(&self, input: ToolInput, _context: ToolContext<'_>) -> ToolResult {
        let plan = input.decode_json::<UpdatePlanArgs>()?;
        if plan
            .plan
            .iter()
            .filter(|item| matches!(item.status, PlanStatus::InProgress))
            .count()
            > 1
        {
            return Ok(ToolExecution::error(
                "update_plan allows at most one in_progress step",
            ));
        }
        if plan.plan.iter().any(|item| item.step.trim().is_empty()) {
            return Ok(ToolExecution::error("update_plan steps must not be empty"));
        }
        let _ = plan.explanation.as_deref();
        *self.current.lock().await = Some(plan);
        Ok(ToolExecution::text("Plan updated").with_code_mode_value(json!({})))
    }
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct UpdatePlanArgs {
    #[serde(default)]
    explanation: Option<String>,
    plan: Vec<PlanItem>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PlanItem {
    step: String,
    status: PlanStatus,
}

#[derive(Deserialize)]
#[serde(rename_all = "snake_case")]
enum PlanStatus {
    Pending,
    InProgress,
    Completed,
}