Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
39 KiB
JSON

{"repo": "bnflyd/weztcode", "files": {"src/chat/backend.rs": {"f51f23700e907f0deda6c421d653b462a2e1c3fe": {"t": 1786068334.4438221, "n": 38111, "h": "84d7bb948b3d97d1", "c": "use std::fs;\nuse std::io::{BufRead, BufReader, Read, Write};\nuse std::path::PathBuf;\nuse std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Stdio};\nuse std::sync::{Arc, Mutex};\nuse std::thread;\n\n#[derive(Clone)]\npub struct ChatConfig {\n pub provider: String,\n pub model: String,\n pub api_key: String,\n pub pi_path: String,\n pub thinking_level: Option<String>,\n}\n\nimpl ChatConfig {\n pub fn from_default_model() -> Self {\n if let Some(model) = crate::config::models::get_default() {\n let resolved_key = crate::config::keys::KeysStore::resolve(&model.api_key);\n Self {\n provider: model.provider,\n model: model.model,\n api_key: resolved_key,\n pi_path: find_pi_path(),\n thinking_level: model.thinking_level.clone(),\n }\n } else {\n Self::from_props()\n }\n }\n\n pub fn from_model_entry(entry: &crate::config::models::ModelEntry) -> Self {\n Self {\n provider: entry.provider.clone(),\n model: entry.model.clone(),\n api_key: crate::config::keys::KeysStore::resolve(&entry.api_key),\n pi_path: find_pi_path(),\n thinking_level: entry.thinking_level.clone(),\n }\n }\n\n pub fn from_sub_agent(entry: &crate::config::sub_agents::SubAgentEntry) -> Self {\n // Intentar resolver por name en models.json (case-insensitive, trimmed)\n let models = crate::config::models::list();\n let agent_model = entry.model.trim();\n if let Some(model_entry) = models.iter().find(|m| m.name.trim().eq_ignore_ascii_case(agent_model)) {\n let resolved_key = crate::config::keys::KeysStore::resolve(&model_entry.api_key);\n return Self {\n provider: model_entry.provider.clone(),\n model: model_entry.model.clone(),\n api_key: resolved_key,\n pi_path: find_pi_path(),\n thinking_level: model_entry.thinking_level.clone(),\n };\n }\n\n // Fallback: usar provider y api_key de user_props.lua\n let props = crate::config::props::UserProps::load();\n let provider = props.get(\"llm_provider\").unwrap_or(\"openrouter\").to_string();\n let api_key = props.get_resolved(\"llm_api_key\").unwrap_or_default();\n Self {\n provider,\n model: entry.model.clone(),\n api_key,\n pi_path: find_pi_path(),\n thinking_level: None,\n }\n }\n\n pub fn from_props() -> Self {\n let props = crate::config::props::UserProps::load();\n Self {\n provider: props.get(\"llm_provider\").unwrap_or(\"openrouter\").to_string(),\n model: props.get(\"llm_model\").unwrap_or(\"openrouter/anthropic/claude-sonnet-4\").to_string(),\n api_key: props.get_resolved(\"llm_api_key\").unwrap_or_default(),\n pi_path: find_pi_path(),\n thinking_level: None,\n }\n }\n}\n\nfn find_pi_path() -> String {\n let props = crate::config::props::UserProps::load();\n if let Some(path) = props.get(\"pi_path\").filter(|s| !s.is_empty()) {\n let resolved = path.to_string();\n // eprintln!(\"[pi] find_pi_path: explicit pi_path = {}\", resolved);\n return resolved;\n }\n\n let home = std::env::var(\"HOME\").unwrap_or_default();\n let candidates = [\n format!(\"{}/.local/share/pnpm/pi\", home),\n format!(\"{}/.npm-global/bin/pi\", home),\n \"/usr/local/bin/pi\".to_string(),\n \"/usr/bin/pi\".to_string(),\n ];\n\n for candidate in &candidates {\n if std::path::Path::new(candidate).exists() {\n // eprintln!(\"[pi] find_pi_path: found at {}\", candidate);\n return candidate.to_string();\n }\n }\n\n let cwd = std::env::current_dir().unwrap_or_default();\n let local_pi = cwd.join(\"node_modules/.bin/pi\");\n if local_pi.exists() {\n let resolved = local_pi.to_string_lossy().to_string();\n // eprintln!(\"[pi] find_pi_path: found local at {}\", resolved);\n return resolved;\n }\n\n // eprintln!(\"[pi] find_pi_path: fallback to 'pi' (PATH lookup)\");\n \"pi\".to_string()\n}\n\npub fn sync_pi_model_overrides() -> Result<(), String> {\n let models = crate::config::models::list();\n let with_reasoning: Vec<_> = models.iter()\n .filter(|m| m.reasoning.unwrap_or(false))\n .collect();\n\n if with_reasoning.is_empty() {\n return Ok(());\n }\n\n let home = std::env::var(\"HOME\")\n .or_else(|_| std::env::var(\"USERPROFILE\"))\n .map_err(|_| \"HOME/USERPROFILE not set\".to_string())?;\n let pi_dir = PathBuf::from(&home).join(\".pi/agent\");\n let pi_models_path = pi_dir.join(\"models.json\");\n\n let mut config: serde_json::Value = if pi_models_path.exists() {\n let content = fs::read_to_string(&pi_models_path)\n .map_err(|e| format!(\"Failed to read {}: {}\", pi_models_path.display(), e))?;\n serde_json::from_str(&content).unwrap_or(serde_json::json!({}))\n } else {\n serde_json::json!({})\n };\n\n if !config.is_object() {\n config = serde_json::json!({});\n }\n if config.get(\"providers\").is_none() {\n config[\"providers\"] = serde_json::json!({});\n }\n\n for model in &with_reasoning {\n let provider = &model.provider;\n let model_name = &model.model;\n\n let provider_entry = config[\"providers\"]\n .as_object_mut()\n .ok_or_else(|| \"providers not an object\".to_string())?\n .entry(provider.clone())\n .or_insert_with(|| serde_json::json!({}));\n let provider_obj = provider_entry.as_object_mut()\n .ok_or_else(|| format!(\"provider {} entry not an object\", provider))?;\n\n let overrides = provider_obj.entry(\"modelOverrides\")\n .or_insert_with(|| serde_json::json!({}));\n let overrides_obj = overrides.as_object_mut()\n .ok_or_else(|| \"modelOverrides not an object\".to_string())?;\n\n let model_entry = overrides_obj.entry(model_name.clone())\n .or_insert_with(|| serde_json::json!({}));\n let model_obj = model_entry.as_object_mut()\n .ok_or_else(|| format!(\"model {} entry not an object\", model_name))?;\n\n model_obj.insert(\"reasoning\".to_string(), serde_json::json!(true));\n }\n\n fs::create_dir_all(&pi_dir)\n .map_err(|e| format!(\"Failed to create {}: {}\", pi_dir.display(), e))?;\n\n let content = serde_json::to_string_pretty(&config)\n .map_err(|e| format!(\"JSON serialize: {}\", e))?;\n fs::write(&pi_models_path, &content)\n .map_err(|e| format!(\"Failed to write {}: {}\", pi_models_path.display(), e))?;\n\n // eprintln!(\"[pi] synced model overrides to {}\", pi_models_path.display());\n Ok(())\n}\n\n#[derive(Debug, Clone)]\npub enum SseEvent {\n Token { content: String },\n ToolCall { name: String },\n ToolResult { name: String, status: String },\n Warning { message: String },\n Error { message: String },\n SessionStats { json: String },\n Done,\n}\n\nimpl SseEvent {\n pub fn to_sse_string(&self) -> String {\n match self {\n SseEvent::Token { content } => {\n serde_json::json!({\"type\":\"token\",\"content\":content}).to_string()\n }\n SseEvent::ToolCall { name } => {\n serde_json::json!({\"type\":\"tool_call\",\"name\":name}).to_string()\n }\n SseEvent::ToolResult { name, status } => {\n serde_json::json!({\"type\":\"tool_result\",\"name\":name,\"status\":status}).to_string()\n }\n SseEvent::Warning { message } => {\n serde_json::json!({\"type\":\"warning\",\"message\":message}).to_string()\n }\n SseEvent::Error { message } => {\n serde_json::json!({\"type\":\"error\",\"message\":message}).to_string()\n }\n SseEvent::SessionStats { json } => {\n serde_json::json!({\"type\":\"session_stats\",\"json\":json}).to_string()\n }\n SseEvent::Done => {\n serde_json::json!({\"type\":\"done\"}).to_string()\n }\n }\n }\n}\n\npub trait AgentBackend: Send {\n fn spawn(&mut self) -> Result<(), String>;\n fn send_message(&mut self, message: &str) -> Result<tokio::sync::mpsc::Receiver<SseEvent>, String>;\n fn shutdown(&mut self);\n\n fn restart(&mut self) -> Result<(), String> {\n self.shutdown();\n self.spawn()\n }\n\n fn get_session_stats(&self) -> Result<String, String> {\n Err(\"get_session_stats not supported by this backend\".to_string())\n }\n fn get_state(&self) -> Result<String, String> {\n Err(\"get_state not supported by this backend\".to_string())\n }\n fn new_session(&mut self) -> Result<(), String> {\n Err(\"new_session not supported by this backend\".to_string())\n }\n\n fn set_agent_prompt(&mut self, _prompt: Option<String>) {}\n\n fn set_model_rpc(&mut self, _provider: &str, _model_id: &str) -> Result<(), String> {\n Err(\"set_model_rpc not supported by this backend\".to_string())\n }\n\n fn config(&self) -> &ChatConfig;\n}\n\nfn env_var_for_provider(provider: &str) -> &'static str {\n match provider {\n \"opencode\" | \"opencode-go\" => \"OPENCODE_API_KEY\",\n \"deepseek\" => \"DEEPSEEK_API_KEY\",\n \"openrouter\" => \"OPENROUTER_API_KEY\",\n \"anthropic\" => \"ANTHROPIC_API_KEY\",\n \"openai\" => \"OPENAI_API_KEY\",\n \"google\" => \"GEMINI_API_KEY\",\n \"mistral\" => \"MISTRAL_API_KEY\",\n \"groq\" => \"GROQ_API_KEY\",\n \"xai\" => \"XAI_API_KEY\",\n _ => \"OPENROUTER_API_KEY\",\n }\n}\n\npub struct PiAgentBackend {\n config: ChatConfig,\n child: Option<Child>,\n stdin: Option<Arc<Mutex<ChildStdin>>>,\n stdout: Option<Arc<Mutex<ChildStdout>>>,\n stderr: Option<Arc<Mutex<ChildStderr>>>,\n thinking_configured: bool,\n current_agent_prompt: Option<String>,\n}\n\nimpl PiAgentBackend {\n pub fn new(config: ChatConfig) -> Self {\n Self {\n config,\n child: None,\n stdin: None,\n stdout: None,\n stderr: None,\n thinking_configured: false,\n current_agent_prompt: None,\n }\n }\n\n pub fn set_agent_prompt(&mut self, prompt: Option<String>) {\n self.current_agent_prompt = prompt;\n }\n\n pub fn config(&self) -> &ChatConfig {\n &self.config\n }\n\n pub fn get_state(&self) -> Result<String, String> {\n let stdin = self.stdin.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdin is None)\".to_string())?;\n let stdout_arc = self.stdout.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdout is None)\".to_string())?\n .clone();\n\n let msg = serde_json::json!({\"type\": \"get_state\"});\n let msg_str = serde_json::to_string(&msg).map_err(|e| format!(\"JSON serialize: {}\", e))?;\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", msg_str)\n .map_err(|e| format!(\"Failed to write get_state: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush get_state: {}\", e))?;\n }\n\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => return Err(format!(\"stdout read error: {}\", e)),\n }\n }\n\n String::from_utf8(bytes).map_err(|e| format!(\"Invalid UTF-8: {}\", e))\n }\n\n pub fn get_session_stats(&self) -> Result<String, String> {\n let stdin = self.stdin.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdin is None)\".to_string())?;\n let stdout_arc = self.stdout.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdout is None)\".to_string())?\n .clone();\n\n let msg = serde_json::json!({\"type\": \"get_session_stats\"});\n let msg_str = serde_json::to_string(&msg).map_err(|e| format!(\"JSON serialize: {}\", e))?;\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", msg_str)\n .map_err(|e| format!(\"Failed to write get_session_stats: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush get_session_stats: {}\", e))?;\n }\n\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => return Err(format!(\"stdout read error: {}\", e)),\n }\n }\n\n String::from_utf8(bytes).map_err(|e| format!(\"Invalid UTF-8: {}\", e))\n }\n\n pub fn new_session(&mut self) -> Result<(), String> {\n let stdin = self.stdin.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdin is None)\".to_string())?;\n let stdout_arc = self.stdout.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdout is None)\".to_string())?\n .clone();\n\n let msg = serde_json::json!({\"type\": \"new_session\"});\n let msg_str = serde_json::to_string(&msg).map_err(|e| format!(\"JSON serialize: {}\", e))?;\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", msg_str)\n .map_err(|e| format!(\"Failed to write new_session: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush new_session: {}\", e))?;\n }\n\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => return Err(format!(\"stdout read error: {}\", e)),\n }\n }\n\n let response = String::from_utf8(bytes).map_err(|e| format!(\"Invalid UTF-8: {}\", e))?;\n let json: serde_json::Value = serde_json::from_str(&response)\n .map_err(|e| format!(\"JSON parse error: {}\", e))?;\n\n let success = json.get(\"success\").and_then(|v| v.as_bool()).unwrap_or(false);\n if success {\n Ok(())\n } else {\n let err = json.get(\"error\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown error\");\n Err(err.to_string())\n }\n }\n\n pub fn set_model_rpc(&mut self, provider: &str, model_id: &str) -> Result<(), String> {\n let stdin = self.stdin.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdin is None)\".to_string())?;\n let stdout_arc = self.stdout.as_ref()\n .ok_or_else(|| \"Pi not spawned (stdout is None)\".to_string())?\n .clone();\n\n let msg = serde_json::json!({\n \"type\": \"set_model\",\n \"provider\": provider,\n \"modelId\": model_id,\n });\n let msg_str = serde_json::to_string(&msg).map_err(|e| format!(\"JSON serialize: {}\", e))?;\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", msg_str)\n .map_err(|e| format!(\"Failed to write set_model: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush set_model: {}\", e))?;\n }\n\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => return Err(format!(\"stdout read error: {}\", e)),\n }\n }\n\n let response = String::from_utf8(bytes).map_err(|e| format!(\"Invalid UTF-8: {}\", e))?;\n let json: serde_json::Value = serde_json::from_str(&response)\n .map_err(|e| format!(\"JSON parse error: {}\", e))?;\n\n let success = json.get(\"success\").and_then(|v| v.as_bool()).unwrap_or(false);\n if success {\n self.config.model = model_id.to_string();\n self.config.provider = provider.to_string();\n Ok(())\n } else {\n let err = json.get(\"error\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown\");\n Err(format!(\"set_model failed: {}\", err))\n }\n }\n}\n\nimpl AgentBackend for PiAgentBackend {\n fn get_session_stats(&self) -> Result<String, String> {\n PiAgentBackend::get_session_stats(self)\n }\n\n fn get_state(&self) -> Result<String, String> {\n PiAgentBackend::get_state(self)\n }\n\n fn new_session(&mut self) -> Result<(), String> {\n PiAgentBackend::new_session(self)\n }\n\n fn set_agent_prompt(&mut self, prompt: Option<String>) {\n PiAgentBackend::set_agent_prompt(self, prompt);\n }\n\n fn set_model_rpc(&mut self, provider: &str, model_id: &str) -> Result<(), String> {\n PiAgentBackend::set_model_rpc(self, provider, model_id)\n }\n\n fn config(&self) -> &ChatConfig {\n PiAgentBackend::config(self)\n }\n\n fn send_message(&mut self, message: &str) -> Result<tokio::sync::mpsc::Receiver<SseEvent>, String> {\n let (tx, rx) = tokio::sync::mpsc::channel(64);\n\n let stdin = self.stdin.as_ref()\n .ok_or_else(|| {\n let msg = \"Pi not spawned (stdin is None)\".to_string();\n eprintln!(\"[pi] send_message: {}\", msg);\n msg\n })?;\n let stdout_arc = self.stdout.as_ref()\n .ok_or_else(|| {\n let msg = \"Pi not spawned (stdout is None)\".to_string();\n eprintln!(\"[pi] send_message: {}\", msg);\n msg\n })?\n .clone();\n let stderr_arc = self.stderr.as_ref().map(Arc::clone);\n\n if let Some(level) = &self.config.thinking_level {\n if !self.thinking_configured {\n let pi_level = match level.as_str() {\n \"max\" => \"xhigh\",\n other => other,\n };\n\n // --- Send set_thinking_level RPC ---\n let set_msg = serde_json::json!({\n \"type\": \"set_thinking_level\",\n \"level\": pi_level,\n });\n let set_str = serde_json::to_string(&set_msg).unwrap_or_default();\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", set_str)\n .map_err(|e| format!(\"Failed to write set_thinking_level: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush set_thinking_level: {}\", e))?;\n }\n\n // --- Read set_thinking_level response synchronously ---\n {\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => {\n // eprintln!(\"[pi] error reading set_thinking_level response: {}\", e);\n break;\n }\n }\n }\n if !bytes.is_empty() {\n let response = String::from_utf8_lossy(&bytes);\n if let Ok(json) = serde_json::from_str::<serde_json::Value>(&response) {\n let success = json.get(\"success\").and_then(|v| v.as_bool()).unwrap_or(false);\n if success {\n // eprintln!(\"[pi] set_thinking_level confirmed: {} ✓\", pi_level);\n } else {\n let err = json.get(\"error\").and_then(|v| v.as_str()).unwrap_or(\"unknown\");\n // eprintln!(\"[pi] set_thinking_level FAILED: {} (level: {})\", err, pi_level);\n }\n } else {\n // eprintln!(\"[pi] set_thinking_level response (unparsed): {}\", response);\n }\n }\n }\n\n // --- Send get_state RPC to verify actual level post-clamping ---\n let get_state_msg = serde_json::json!({\"type\": \"get_state\"});\n let get_state_str = serde_json::to_string(&get_state_msg).unwrap_or_default();\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", get_state_str)\n .map_err(|e| format!(\"Failed to write get_state: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush get_state: {}\", e))?;\n }\n\n // --- Read get_state response ---\n {\n let mut stdout_lock = stdout_arc.lock().map_err(|e| format!(\"stdout lock: {}\", e))?;\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(e) => {\n // eprintln!(\"[pi] error reading get_state response: {}\", e);\n break;\n }\n }\n }\n if !bytes.is_empty() {\n // get_state verification (logged via main.rs after each message)\n }\n }\n\n self.thinking_configured = true;\n // eprintln!(\"[pi] thinking_level configured successfully\");\n }\n }\n\n let final_message = if let Some(prompt) = self.current_agent_prompt.take() {\n format!(\"[System instructions]\\n{}\\n\\n---\\n\\n{}\", prompt, message)\n } else {\n message.to_string()\n };\n\n let request = serde_json::json!({\n \"type\": \"prompt\",\n \"message\": final_message,\n });\n\n let request_str = serde_json::to_string(&request).unwrap_or_default();\n\n {\n let mut stdin_lock = stdin.lock().map_err(|e| format!(\"stdin lock: {}\", e))?;\n writeln!(stdin_lock, \"{}\", request_str)\n .map_err(|e| format!(\"Failed to write to pi stdin: {}\", e))?;\n stdin_lock.flush()\n .map_err(|e| format!(\"Failed to flush pi stdin: {}\", e))?;\n }\n\n // Thread for stderr: capture pi warnings/errors and forward them as SseEvent::Error\n if let Some(stderr_arc) = stderr_arc {\n let tx_err = tx.clone();\n thread::spawn(move || {\n let mut stderr_lock = match stderr_arc.lock() {\n Ok(g) => g,\n Err(_) => return,\n };\n let mut reader = BufReader::new(&mut *stderr_lock);\n let mut line = String::new();\n loop {\n line.clear();\n match reader.read_line(&mut line) {\n Ok(0) | Err(_) => break,\n Ok(_) => {\n let trimmed = line.trim();\n if !trimmed.is_empty() {\n let _ = tx_err.blocking_send(SseEvent::Warning {\n message: trimmed.to_string(),\n });\n }\n }\n }\n }\n });\n }\n\n let stdin_arc = stdin.clone();\n\n // Thread for stdout: parse JSON event stream\n thread::spawn(move || {\n let mut stdout_lock = match stdout_arc.lock() {\n Ok(g) => g,\n Err(_) => {\n eprintln!(\"[pi] reader: stdout lock failed\");\n let _ = tx.blocking_send(SseEvent::Error {\n message: \"stdout lock failed\".to_string(),\n });\n let _ = tx.blocking_send(SseEvent::Done);\n return;\n }\n };\n\n let mut reader = BufReader::new(&mut *stdout_lock);\n let mut line = String::new();\n let mut agent_ended = false;\n\n loop {\n line.clear();\n match reader.read_line(&mut line) {\n Ok(0) => {\n break;\n }\n Err(e) => {\n eprintln!(\"[pi] reader: read error: {}\", e);\n break;\n }\n Ok(n) => {\n let trimmed = line.trim();\n if trimmed.is_empty() {\n continue;\n }\n\n match serde_json::from_str::<serde_json::Value>(trimmed) {\n Ok(json) => {\n let event_type = json.get(\"type\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"\");\n\n match event_type {\n \"response\" => {\n let success = json.get(\"success\")\n .and_then(|v| v.as_bool())\n .unwrap_or(false);\n if !success {\n let err = json.get(\"error\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"Unknown error\");\n let _ = tx.blocking_send(SseEvent::Error {\n message: err.to_string(),\n });\n break;\n }\n }\n \"message_update\" => {\n if let Some(ae) = json.get(\"assistantMessageEvent\") {\n match ae.get(\"type\").and_then(|v| v.as_str()) {\n Some(\"text_delta\") => {\n if let Some(delta) = ae.get(\"delta\").and_then(|v| v.as_str()) {\n let _ = tx.blocking_send(SseEvent::Token {\n content: delta.to_string(),\n });\n }\n }\n Some(\"toolcall_end\") => {\n if let Some(tc) = ae.get(\"toolCall\") {\n let name = tc.get(\"name\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"tool\");\n let _ = tx.blocking_send(SseEvent::ToolCall {\n name: name.to_string(),\n });\n }\n }\n Some(\"error\") => {\n let reason = ae.get(\"reason\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"unknown\");\n let _ = tx.blocking_send(SseEvent::Error {\n message: format!(\"Stream error: {}\", reason),\n });\n }\n _ => {}\n }\n }\n }\n \"tool_execution_start\" => {\n let name = json.get(\"toolName\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"tool\");\n let _ = tx.blocking_send(SseEvent::ToolCall {\n name: name.to_string(),\n });\n }\n \"tool_execution_end\" => {\n let name = json.get(\"toolName\")\n .and_then(|v| v.as_str())\n .unwrap_or(\"tool\");\n let is_err = json.get(\"isError\")\n .and_then(|v| v.as_bool())\n .unwrap_or(false);\n let _ = tx.blocking_send(SseEvent::ToolResult {\n name: name.to_string(),\n status: if is_err { \"error\" } else { \"ok\" }.to_string(),\n });\n }\n \"agent_end\" => {\n agent_ended = true;\n break;\n }\n \"error\" => {\n let msg = json.get(\"error\")\n .and_then(|v| v.as_str())\n .or_else(|| json.get(\"message\").and_then(|v| v.as_str()))\n .unwrap_or(\"Unknown error\");\n let _ = tx.blocking_send(SseEvent::Error {\n message: msg.to_string(),\n });\n break;\n }\n _ => {}\n }\n }\n Err(_) => {\n let _ = tx.blocking_send(SseEvent::Token {\n content: trimmed.to_string(),\n });\n }\n }\n }\n }\n }\n\n // Release stdout lock before doing RPC\n drop(reader);\n drop(stdout_lock);\n\n // After stream ends, fetch and send real session stats\n if agent_ended {\n let msg = serde_json::json!({\"type\": \"get_session_stats\"});\n if let Ok(s) = serde_json::to_string(&msg) {\n if let Ok(lock) = stdin_arc.lock() {\n let _ = writeln!(&*lock, \"{}\", s);\n let _ = (&*lock).flush();\n }\n }\n\n if let Ok(mut stdout_lock) = stdout_arc.lock() {\n let mut bytes = Vec::new();\n let mut buf = [0u8; 1];\n loop {\n match stdout_lock.read(&mut buf) {\n Ok(0) => break,\n Ok(_) => {\n if buf[0] == b'\\n' { break; }\n bytes.push(buf[0]);\n }\n Err(_) => break,\n }\n }\n if !bytes.is_empty() {\n if let Ok(s) = String::from_utf8(bytes) {\n let _ = tx.blocking_send(SseEvent::SessionStats { json: s });\n }\n }\n }\n }\n\n let _ = tx.blocking_send(SseEvent::Done);\n });\n\n Ok(rx)\n }\n\n fn spawn(&mut self) -> Result<(), String> {\n let mut cmd = Command::new(&self.config.pi_path);\n cmd.args([\"--mode\", \"rpc\"])\n .arg(\"--provider\").arg(&self.config.provider)\n .arg(\"--model\").arg(&self.config.model);\n\n cmd.stdin(Stdio::piped())\n .stdout(Stdio::piped())\n .stderr(Stdio::piped());\n\n let env_key = env_var_for_provider(&self.config.provider);\n cmd.env(env_key, &self.config.api_key);\n cmd.env(\"PI_CACHE_RETENTION\", \"long\");\n\n // eprintln!(\"[pi] spawn: path={}, provider={}, model={}\", self.config.pi_path, self.config.provider, self.config.model);\n\n cmd.current_dir(crate::config::current_root::get());\n let mut child = cmd.spawn().map_err(|e| {\n let msg = format!(\"Failed to spawn pi: {}\", e);\n eprintln!(\"[pi] {}\", msg);\n msg\n })?;\n\n // eprintln!(\"[pi] spawned with PID={}\", child.id());\n\n let stdin = child.stdin.take()\n .ok_or_else(|| {\n let msg = \"Failed to capture pi stdin\".to_string();\n eprintln!(\"[pi] {}\", msg);\n msg\n })?;\n let stdout = child.stdout.take()\n .ok_or_else(|| {\n let msg = \"Failed to capture pi stdout\".to_string();\n eprintln!(\"[pi] {}\", msg);\n msg\n })?;\n let stderr = child.stderr.take()\n .ok_or_else(|| {\n let msg = \"Failed to capture pi stderr\".to_string();\n eprintln!(\"[pi] {}\", msg);\n msg\n })?;\n\n // Pequeña pausa para detectar si pi muere inmediatamente (como rpc-client.ts)\n std::thread::sleep(std::time::Duration::from_millis(200));\n match child.try_wait() {\n Ok(Some(status)) => {\n let msg = format!(\"pi exited immediately with status={}\", status);\n eprintln!(\"[pi] spawn: {}\", msg);\n return Err(msg);\n }\n Ok(None) => {\n // eprintln!(\"[pi] spawn: pi is still running after 200ms, good\");\n }\n Err(e) => {\n eprintln!(\"[pi] spawn: try_wait error: {}\", e);\n }\n }\n\n self.stdin = Some(Arc::new(Mutex::new(stdin)));\n self.stdout = Some(Arc::new(Mutex::new(stdout)));\n self.stderr = Some(Arc::new(Mutex::new(stderr)));\n self.child = Some(child);\n Ok(())\n }\n\n fn shutdown(&mut self) {\n self.stdin = None;\n self.stdout = None;\n self.stderr = None;\n self.thinking_configured = false;\n if let Some(mut child) = self.child.take() {\n let _ = child.kill();\n let _ = child.wait();\n }\n }\n}\n\npub struct NullBackend {\n config: ChatConfig,\n}\n\nimpl NullBackend {\n pub fn new() -> Self {\n Self {\n config: ChatConfig {\n provider: String::new(),\n model: String::new(),\n api_key: String::new(),\n pi_path: find_pi_path(),\n thinking_level: None,\n },\n }\n }\n}\n\nimpl AgentBackend for NullBackend {\n fn spawn(&mut self) -> Result<(), String> {\n Ok(())\n }\n\n fn new_session(&mut self) -> Result<(), String> {\n Ok(())\n }\n\n fn send_message(&mut self, message: &str) -> Result<tokio::sync::mpsc::Receiver<SseEvent>, String> {\n let (tx, rx) = tokio::sync::mpsc::channel(64);\n let msg = message.to_string();\n thread::spawn(move || {\n let _ = tx.blocking_send(SseEvent::Token {\n content: format!(\"[echo] {}\\n\\n(Conecta Pi Agent para respuestas reales)\", msg),\n });\n let _ = tx.blocking_send(SseEvent::Done);\n });\n Ok(rx)\n }\n\n fn set_agent_prompt(&mut self, _prompt: Option<String>) {}\n\n fn config(&self) -> &ChatConfig {\n &self.config\n }\n\n fn shutdown(&mut self) {}\n}\n"}}}}