1use 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
12pub 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 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
96pub 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 = crate::http::shared();
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 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 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}