Skip to main content

apollo/tools/
session.rs

1//! Session tools — model switching, status, config management.
2//! Gives the AI control over its own session (like OpenClaw's session_status).
3
4use async_trait::async_trait;
5use serde::Deserialize;
6use std::sync::Arc;
7
8use super::traits::*;
9use crate::agent::AgentRunner;
10use crate::text::truncate_chars;
11
12/// session_status — view/change model, check status
13pub struct SessionStatusTool {
14    runner: Arc<AgentRunner>,
15}
16
17impl SessionStatusTool {
18    pub fn new(runner: Arc<AgentRunner>) -> Self {
19        Self { runner }
20    }
21}
22
23#[derive(Deserialize)]
24struct SessionStatusArgs {
25    /// Set model override (e.g. "claude-opus-4", "claude-haiku-3-5")
26    model: Option<String>,
27}
28
29#[async_trait]
30impl Tool for SessionStatusTool {
31    fn name(&self) -> &str {
32        "session_status"
33    }
34
35    fn spec(&self) -> ToolSpec {
36        ToolSpec {
37            name: "session_status".to_string(),
38            description: "Show session status (current model, tools, uptime). Optionally set model override with model parameter.".to_string(),
39            parameters: serde_json::json!({
40                "type": "object",
41                "properties": {
42                    "model": {
43                        "type": "string",
44                        "description": "Set model override (e.g. 'claude-opus-4', 'claude-sonnet-4-5', 'claude-haiku-3-5'). Use 'default' to reset."
45                    }
46                }
47            }),
48        }
49    }
50
51    async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
52        let args: SessionStatusArgs =
53            serde_json::from_str(arguments).unwrap_or(SessionStatusArgs { model: None });
54
55        if let Some(model) = &args.model {
56            if model == "default" || model == "reset" {
57                let default = self.runner.get_default_model().to_string();
58                self.runner.reset_model();
59                return Ok(ToolResult::success(format!(
60                    "Model reset to configured default: {default}"
61                )));
62            }
63            self.runner.set_model(model.as_str());
64            return Ok(ToolResult::success(format!("Model switched to: {model}")));
65        }
66
67        let tools = self.runner.list_tools().await;
68        let cfg = &self.runner.agent_config;
69        let status = format!(
70            "Session Status:\n\
71            Model (current):  {}\n\
72            Model (default):  {}\n\
73            Model (fast):     {}\n\
74            Model (heavy):    {}\n\
75            Tools: {} active\n\
76            PID: {}\n\
77            Runtime: apollo v{}\n\n\
78            Tool list: {}\n\n\
79            Tip: use session_status{{\"model\":\"...\"}}\n\
80            For swarms — use fast model as runner, heavy as orchestrator.\n\
81            Available aliases: default/reset (restore configured model)",
82            self.runner.get_model(),
83            self.runner.get_default_model(),
84            cfg.fast_model,
85            cfg.heavy_model,
86            tools.len(),
87            std::process::id(),
88            env!("CARGO_PKG_VERSION"),
89            tools.join(", "),
90        );
91
92        Ok(ToolResult::success(status))
93    }
94}
95
96/// list_models — fetch available models from Anthropic API
97pub struct ListModelsTool;
98
99impl ListModelsTool {
100    pub fn new() -> Self {
101        Self
102    }
103}
104
105impl Default for ListModelsTool {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111#[async_trait]
112impl Tool for ListModelsTool {
113    fn name(&self) -> &str {
114        "list_models"
115    }
116
117    fn spec(&self) -> ToolSpec {
118        ToolSpec {
119            name: "list_models".to_string(),
120            description: "List available Claude models from the Anthropic API. Use this to discover the latest models.".to_string(),
121            parameters: serde_json::json!({
122                "type": "object",
123                "properties": {}
124            }),
125        }
126    }
127
128    async fn execute(&self, _arguments: &str) -> anyhow::Result<ToolResult> {
129        let api_key = std::env::var("ANTHROPIC_API_KEY").unwrap_or_default();
130        if api_key.is_empty() {
131            return Ok(ToolResult::error("No ANTHROPIC_API_KEY set"));
132        }
133
134        let is_oauth = api_key.contains("sk-ant-oat");
135        let client = reqwest::Client::new();
136
137        let mut req = client
138            .get("https://api.anthropic.com/v1/models")
139            .header("anthropic-version", "2023-06-01");
140
141        if is_oauth {
142            req = req
143                .header("Authorization", format!("Bearer {}", api_key))
144                .header("anthropic-beta", "claude-code-20250219,oauth-2025-04-20");
145        } else {
146            req = req.header("x-api-key", &api_key);
147        }
148
149        let resp = req.send().await?;
150
151        if !resp.status().is_success() {
152            let status = resp.status();
153            let text = resp.text().await.unwrap_or_default();
154            return Ok(ToolResult::error(format!(
155                "API error {}: {}",
156                status,
157                truncate_chars(&text, 300)
158            )));
159        }
160
161        let data: serde_json::Value = resp.json().await?;
162
163        // Parse model list
164        let models = data["data"].as_array();
165        match models {
166            Some(list) => {
167                let mut output = String::from("Available models:\n\n");
168                for m in list {
169                    let id = m["id"].as_str().unwrap_or("unknown");
170                    let display = m["display_name"].as_str().unwrap_or(id);
171                    output.push_str(&format!("• {} ({})\n", display, id));
172                }
173                Ok(ToolResult::success(output))
174            }
175            None => {
176                // Fallback: return known models
177                Ok(ToolResult::success(
178                    "Available models:\n\n\
179                    • claude-sonnet-4-6 (balanced — default)\n\
180                    • claude-opus-4-6 (most capable)\n\
181                    • claude-haiku-4-5-20251001 (fastest, cheapest — use as swarm runner)\n\n\
182                    Use session_status with model parameter to switch.",
183                ))
184            }
185        }
186    }
187}