navi-core 0.3.1

Local agentic engine and terminal-first coding agent.
Documentation
use anyhow::Result;
use async_trait::async_trait;
use serde_json::json;

use super::helpers;
use crate::event::AgentEvent;
use crate::tool::{
    Tool, ToolDefinition, ToolInvocation, ToolInvocationContext, ToolKind, ToolResult,
};

pub(crate) struct SetGoalTool;

#[async_trait]
impl Tool for SetGoalTool {
    fn definition(&self) -> ToolDefinition {
        helpers::definition(
            "set_goal",
            "Set a long-running goal. The agent will continue running turns autonomously until the goal is achieved, blocked, or the budget is exhausted. Use this for complex, multi-step tasks that require no human intervention.",
            ToolKind::Command,
            json!({
                "type": "object",
                "properties": {
                    "objective": {
                        "type": "string",
                        "description": "A clear, actionable description of the goal to achieve."
                    },
                    "short_description": {
                        "type": "string",
                        "maxLength": 40,
                        "description": "Short TUI label for this goal. Summarize the goal in a few words; maximum 40 characters."
                    },
                    "token_budget": {
                        "type": "integer",
                        "description": "Optional token limit for the goal. If omitted, a default budget is applied."
                    }
                },
                "required": ["objective"],
                "additionalProperties": false
            }),
        )
    }

    async fn invoke(&self, _invocation: ToolInvocation) -> Result<ToolResult> {
        unreachable!("SetGoalTool requires context")
    }

    async fn invoke_with_context(
        &self,
        invocation: ToolInvocation,
        context: ToolInvocationContext,
    ) -> Result<ToolResult> {
        let objective = invocation
            .input
            .get("objective")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        let short_description = invocation
            .input
            .get("short_description")
            .and_then(|v| v.as_str())
            .map(limit_short_description);

        let token_budget = invocation
            .input
            .get("token_budget")
            .and_then(|v| v.as_i64());

        if let Some(tx) = context.event_tx {
            let _ = tx.send(AgentEvent::SetGoalRequested {
                objective: objective.clone(),
                short_description: short_description.clone(),
                token_budget,
            });
        }

        Ok(ToolResult {
            invocation_id: invocation.id,
            ok: true,
            output: json!({
                "message": format!("Goal set: {}. The agent will now run autonomously.", objective)
            }),
        })
    }
}

fn limit_short_description(value: &str) -> String {
    value.trim().chars().take(40).collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::event::AgentEvent;
    use serde_json::json;

    #[tokio::test]
    async fn set_goal_emits_short_description_limited_to_40_chars() {
        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
        let tool = SetGoalTool;

        let result = tool
            .invoke_with_context(
                ToolInvocation {
                    id: "goal-call".to_string(),
                    tool_name: "set_goal".to_string(),
                    input: json!({
                        "objective": "Fix the TUI layout",
                        "short_description": "1234567890123456789012345678901234567890extra",
                    }),
                },
                ToolInvocationContext {
                    event_tx: Some(tx),
                    ..Default::default()
                },
            )
            .await
            .expect("set goal");

        assert!(result.ok);
        let event = rx.recv().await.expect("goal event");
        let AgentEvent::SetGoalRequested {
            short_description, ..
        } = event
        else {
            panic!("unexpected event");
        };
        assert_eq!(
            short_description.as_deref(),
            Some("1234567890123456789012345678901234567890")
        );
    }
}