1use anyhow::{Context, Result};
6use std::path::Path;
7
8use super::chat_session::ChatSession;
9use super::skills;
10use super::types::*;
11
12pub async fn run_single_task(
18 workspace: &str,
19 session_key: &str,
20 description: &str,
21 skill_dirs: Option<&[String]>,
22) -> Result<AgentResult> {
23 let mut config = AgentConfig::from_env();
24 config.workspace = workspace.to_string();
25 config.enable_task_planning = false; config.enable_memory = true;
27
28 if config.api_key.is_empty() {
29 anyhow::bail!("API key required for swarm task execution. Set OPENAI_API_KEY.");
30 }
31
32 skilllite_core::config::ensure_default_output_dir();
33
34 let skill_dirs = skill_dirs.map(|s| s.to_vec()).unwrap_or_else(|| {
35 skilllite_core::skill::discovery::discover_skill_dirs_for_loading(
36 Path::new(workspace),
37 Some(&[".skills", "skills"]),
38 )
39 });
40 let loaded_skills = skills::load_skills(&skill_dirs);
41
42 let mut session = ChatSession::new(config, session_key, loaded_skills);
43 let mut sink = SilentEventSink;
44 session.run_turn(description, &mut sink).await
45}
46
47pub fn run_clear_session(session_key: &str, workspace: &str) -> Result<()> {
50 let workspace_path = Path::new(workspace).canonicalize().unwrap_or_else(|_| {
51 std::env::current_dir()
52 .unwrap_or_else(|_| std::path::PathBuf::from("."))
53 .join(workspace)
54 });
55 if std::env::set_current_dir(&workspace_path).is_err() {
56 }
58
59 let mut config = AgentConfig::from_env();
60 config.workspace = workspace_path.to_string_lossy().to_string();
61
62 if config.api_key.is_empty() {
63 tracing::warn!(
64 "No OPENAI_API_KEY; summarization skipped. Session will still be archived and counts reset."
65 );
66 }
67
68 skilllite_core::config::ensure_default_output_dir();
69
70 let loaded_skills = skills::load_skills(&[]);
71 let session_key_owned = session_key.to_string();
72
73 let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
74 rt.block_on(async {
75 let mut session = ChatSession::new_for_clear(config, &session_key_owned, loaded_skills);
76 session.clear_full().await
77 })?;
78
79 Ok(())
80}
81
82pub fn run_chat(
85 config: AgentConfig,
86 session_key: String,
87 single_message: Option<String>,
88) -> Result<()> {
89 skilllite_core::config::ensure_default_output_dir();
90
91 if config.api_key.is_empty() {
92 anyhow::bail!("API key required. Set OPENAI_API_KEY env var or use --api-key flag.");
93 }
94
95 let (effective_skill_dirs, was_auto_discovered) = if config.skill_dirs.is_empty() {
97 let auto_dirs = skilllite_core::skill::discovery::discover_skill_dirs_for_loading(
98 Path::new(&config.workspace),
99 Some(&[".skills", "skills"]),
100 );
101 let has_skills = !auto_dirs.is_empty();
102 (auto_dirs, has_skills)
103 } else {
104 (config.skill_dirs.clone(), false)
105 };
106
107 let loaded_skills = skills::load_skills(&effective_skill_dirs);
109 if !loaded_skills.is_empty() {
110 eprintln!("┌─ Skills ─────────────────────────────────────────────────");
111 if was_auto_discovered {
112 eprintln!("│ 🔍 Auto-discovered {} skill(s)", loaded_skills.len());
113 }
114 let names: Vec<&str> = loaded_skills.iter().map(|s| s.name.as_str()).collect();
115 let list = if names.len() <= 6 {
116 names.join(", ")
117 } else {
118 format!("{} … +{} more", names[..5].join(", "), names.len() - 5)
119 };
120 eprintln!("│ 📦 {}", list);
121 eprintln!("└───────────────────────────────────────────────────────────");
122 }
123
124 let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
125
126 let verbose = config.verbose;
127 if let Some(msg) = single_message {
128 rt.block_on(async {
129 let mut session = ChatSession::new(config, &session_key, loaded_skills);
130 let mut sink = TerminalEventSink::new(verbose);
131 let result = session.run_turn(&msg, &mut sink).await?;
132 println!("\n{}", result.response);
133 Ok(())
134 })
135 } else {
136 rt.block_on(async {
137 run_interactive_chat(config, &session_key, loaded_skills, verbose).await
138 })
139 }
140}
141
142pub fn run_agent_run(config: AgentConfig, goal: String, resume: bool) -> Result<()> {
150 if config.api_key.is_empty() {
151 anyhow::bail!("API key required. Set OPENAI_API_KEY env var or use --api-key flag.");
152 }
153
154 skilllite_core::config::ensure_default_output_dir();
155
156 let (effective_goal, effective_workspace, history_override) = if resume {
158 let chat_root = skilllite_executor::chat_root();
159 match super::run_checkpoint::load_checkpoint(&chat_root)? {
160 Some(cp) => {
161 let resume_msg = super::run_checkpoint::build_resume_message(&cp);
162 let history: Vec<ChatMessage> = cp.messages.into_iter().skip(1).collect();
164 eprintln!("📂 从断点续跑 (run_id: {})", cp.run_id);
165 (resume_msg, cp.workspace, Some(history))
166 }
167 None => {
168 anyhow::bail!("无可用断点。请先运行 `skilllite run --goal \"...\"` 以创建断点。");
169 }
170 }
171 } else {
172 (goal, config.workspace.clone(), None)
173 };
174
175 let mut config = config;
176 config.workspace = effective_workspace;
177
178 let _ = super::soul::Soul::offer_bootstrap_soul_if_missing(
180 &config.workspace,
181 config.soul_path.as_deref(),
182 );
183
184 let (effective_skill_dirs, was_auto_discovered) = if config.skill_dirs.is_empty() {
185 let auto_dirs = skilllite_core::skill::discovery::discover_skill_dirs_for_loading(
186 Path::new(&config.workspace),
187 Some(&[".skills", "skills"]),
188 );
189 let has_skills = !auto_dirs.is_empty();
190 (auto_dirs, has_skills)
191 } else {
192 (config.skill_dirs.clone(), false)
193 };
194
195 let loaded_skills = skills::load_skills(&effective_skill_dirs);
196 if !loaded_skills.is_empty() {
197 eprintln!("┌─ Run mode ───────────────────────────────────────────────");
198 if was_auto_discovered {
199 eprintln!("│ 🔍 Auto-discovered {} skill(s)", loaded_skills.len());
200 }
201 let names: Vec<&str> = loaded_skills.iter().map(|s| s.name.as_str()).collect();
202 let list = if names.len() <= 6 {
203 names.join(", ")
204 } else {
205 format!("{} … +{} more", names[..5].join(", "), names.len() - 5)
206 };
207 eprintln!("│ 📦 {}", list);
208 eprintln!(
209 "│ 🎯 Goal: {}",
210 effective_goal.lines().next().unwrap_or(&effective_goal)
211 );
212 eprintln!("└───────────────────────────────────────────────────────────\n");
213 }
214
215 let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
216
217 let verbose = config.verbose;
218 rt.block_on(async {
219 let mut session = ChatSession::new(config, "run", loaded_skills);
220 let mut sink = RunModeEventSink::new(verbose);
221 let result = if let Some(history) = history_override {
222 session
223 .run_turn_with_history(&effective_goal, &mut sink, history)
224 .await
225 } else {
226 session.run_turn(&effective_goal, &mut sink).await
227 };
228 let _ = result?;
229 Ok(())
231 })
232}
233
234fn format_chat_error(e: &anyhow::Error) -> String {
236 let s = e.to_string();
237 if let Some(json_start) = s.find('{') {
238 let json_part = &s[json_start..];
239 if let Ok(v) = serde_json::from_str::<serde_json::Value>(json_part) {
240 if let Some(msg) = v
241 .get("error")
242 .and_then(|e| e.get("message"))
243 .and_then(|m| m.as_str())
244 {
245 let status = s
246 .strip_prefix("LLM API error (")
247 .and_then(|rest| rest.split(')').next())
248 .unwrap_or("API");
249 return format!("{} 错误: {}", status, msg);
250 }
251 }
252 }
253 if s.len() > 200 {
254 format!("{}…", &s[..200])
255 } else {
256 s
257 }
258}
259
260async fn run_interactive_chat(
261 config: AgentConfig,
262 session_key: &str,
263 skills: Vec<skills::LoadedSkill>,
264 verbose: bool,
265) -> Result<()> {
266 eprintln!("┌────────────────────────────────────────────────────────────");
267 eprintln!("│ 🤖 SkillBox Chat · model: {}", config.model);
268 eprintln!("│ /exit 退出 · /clear 清空 · /compact 压缩历史");
269 eprintln!("└────────────────────────────────────────────────────────────\n");
270
271 let mut session = ChatSession::new(config, session_key, skills);
272 let mut sink = TerminalEventSink::new(verbose);
273
274 let mut rl = rustyline::DefaultEditor::new()
275 .map_err(|e| anyhow::anyhow!("Failed to create line editor: {}", e))?;
276
277 loop {
278 let readline = rl.readline("You> ");
279 match readline {
280 Ok(line) => {
281 let input = line.trim();
282 if input.is_empty() {
283 continue;
284 }
285
286 let _ = rl.add_history_entry(input);
287
288 match input {
289 "/exit" | "/quit" | "/q" => {
290 eprintln!("👋 Bye!");
291 break;
292 }
293 "/clear" => {
294 session.clear().await?;
295 eprintln!("🗑️ Session cleared.");
296 continue;
297 }
298 "/compact" => {
299 eprintln!("📦 Compacting history...");
300 match session.force_compact().await {
301 Ok(true) => eprintln!("✅ History compacted."),
302 Ok(false) => eprintln!("ℹ️ Not enough messages to compact."),
303 Err(e) => eprintln!("❌ Compaction failed: {}", format_chat_error(&e)),
304 }
305 continue;
306 }
307 _ => {}
308 }
309
310 eprintln!();
311 match session.run_turn(input, &mut sink).await {
312 Ok(_) => {
313 eprintln!();
314 }
315 Err(e) => {
316 let msg = format_chat_error(&e);
317 eprintln!("❌ {}", msg);
318 eprintln!();
319 }
320 }
321 }
322 Err(rustyline::error::ReadlineError::Interrupted) => {
323 eprintln!("\n^C");
324 eprintln!("👋 Bye!");
325 break;
326 }
327 Err(rustyline::error::ReadlineError::Eof) => {
328 eprintln!("👋 Bye!");
329 break;
330 }
331 Err(e) => {
332 eprintln!("Error: {}", e);
333 break;
334 }
335 }
336 }
337
338 Ok(())
339}