use std::collections::HashMap; use std::path::{Path, PathBuf}; use async_trait::async_trait; use futures_util::{stream, StreamExt}; use zeroclaw::agent::TurnEvent; use zeroclaw::config::Config as ZeroClawConfig; use zeroclaw::providers::{ self, ChatMessage, ChatRequest, ChatResponse, Provider, }; use zeroclaw::providers::traits::{ ProviderCapabilities, StreamEvent, StreamOptions, StreamResult, }; use crate::compat::browser_script_skill_tool::build_browser_script_skill_tools; use crate::compat::browser_tool_adapter::ZeroClawBrowserTool; use crate::compat::config_adapter::{ build_zeroclaw_config_from_sgclaw_settings, resolve_skills_dir_from_sgclaw_settings, }; use crate::compat::openxml_office_tool::OpenXmlOfficeTool; use crate::compat::screen_html_export_tool::ScreenHtmlExportTool; use crate::config::{DeepSeekSettings, OfficeBackend, SgClawSettings}; use crate::compat::event_bridge::log_entry_for_turn_event; use crate::pipe::{BrowserPipeTool, ConversationMessage, PipeError, Transport}; use crate::runtime::RuntimeEngine; #[derive(Debug, Clone, Default)] pub struct CompatTaskContext { pub conversation_id: Option, pub messages: Vec, pub page_url: Option, pub page_title: Option, } pub fn execute_task( transport: &T, browser_tool: BrowserPipeTool, instruction: &str, task_context: &CompatTaskContext, workspace_root: &Path, settings: &DeepSeekSettings, ) -> Result { let sgclaw_settings = SgClawSettings::from(settings); execute_task_with_sgclaw_settings( transport, browser_tool, instruction, task_context, workspace_root, &sgclaw_settings, ) } pub fn execute_task_with_sgclaw_settings( transport: &T, browser_tool: BrowserPipeTool, instruction: &str, task_context: &CompatTaskContext, workspace_root: &Path, settings: &SgClawSettings, ) -> Result { let config = build_zeroclaw_config_from_sgclaw_settings(workspace_root, settings); let skills_dir = resolve_skills_dir_from_sgclaw_settings(workspace_root, settings); let provider = build_provider(&config)?; let runtime = tokio::runtime::Runtime::new() .map_err(|err| PipeError::Protocol(format!("failed to create tokio runtime: {err}")))?; runtime.block_on(execute_task_with_provider( transport, browser_tool, provider, instruction, task_context, config, skills_dir, settings.clone(), )) } pub async fn execute_task_with_provider( transport: &T, browser_tool: BrowserPipeTool, provider: Box, instruction: &str, task_context: &CompatTaskContext, config: ZeroClawConfig, skills_dir: PathBuf, settings: SgClawSettings, ) -> Result { let engine = RuntimeEngine::new(settings.runtime_profile); let browser_surface_present = engine.browser_surface_enabled(); if let Some(preview) = crate::agent::planner::build_execution_preview( settings.planner_mode, instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), ) { let mut message = preview.summary; if !preview.steps.is_empty() { message.push('\n'); message.push_str(&preview.steps.join("\n")); } transport.send(&crate::pipe::AgentMessage::LogEntry { level: "plan".to_string(), message, })?; } let loaded_skills = engine.loaded_skills(&config, &skills_dir); let loaded_skill_versions = loaded_skills .iter() .map(|skill| (skill.name.clone(), skill.version.clone())) .collect::>(); let loaded_skill_labels = loaded_skills .iter() .map(|skill| format!("{}@{}", skill.name, skill.version)) .collect::>(); if !loaded_skill_labels.is_empty() { transport.send(&crate::pipe::AgentMessage::LogEntry { level: "info".to_string(), message: format!("loaded skills: {}", loaded_skill_labels.join(", ")), })?; } let browser_tool_for_scripts = browser_tool.clone(); let mut tools: Vec> = if browser_surface_present { vec![ Box::new(ZeroClawBrowserTool::new_superrpa(browser_tool.clone())), Box::new(ZeroClawBrowserTool::new(browser_tool)), ] } else { Vec::new() }; if browser_surface_present { tools.extend( build_browser_script_skill_tools(&loaded_skills, browser_tool_for_scripts) .map_err(map_anyhow_to_pipe_error)?, ); } if matches!(settings.office_backend, OfficeBackend::OpenXml) && engine.should_attach_openxml_office_tool(instruction) { tools.push(Box::new(OpenXmlOfficeTool::new(config.workspace_dir.clone()))); } if engine.should_attach_screen_html_export_tool(instruction) { tools.push(Box::new(ScreenHtmlExportTool::new(config.workspace_dir.clone()))); } let mut agent = engine.build_agent( provider, &config, &skills_dir, tools, browser_surface_present, instruction, )?; if let Some(conversation_id) = task_context .conversation_id .as_deref() .map(str::trim) .filter(|value| !value.is_empty()) { agent.set_memory_session_id(Some(conversation_id.to_string())); } let mut seed_messages = Vec::new(); seed_messages.extend(build_seed_history(task_context)); if !seed_messages.is_empty() { agent.seed_history(&seed_messages); } let (event_tx, mut event_rx) = tokio::sync::mpsc::channel::(32); let instruction = engine.build_instruction( instruction, task_context.page_url.as_deref(), task_context.page_title.as_deref(), browser_surface_present, ); let task = tokio::spawn(async move { agent.turn_streamed(&instruction, event_tx).await }); while let Some(event) = event_rx.recv().await { if let Some(log_entry) = log_entry_for_turn_event(&event, &loaded_skill_versions) { transport.send(&log_entry)?; } } task.await .map_err(|err| PipeError::Protocol(format!("zeroclaw task join failed: {err}")))? .map_err(|err| PipeError::Protocol(err.to_string())) } fn build_provider(config: &ZeroClawConfig) -> Result, PipeError> { let provider_name = config.default_provider.as_deref().unwrap_or("deepseek"); let model_name = config .default_model .as_deref() .unwrap_or("deepseek-chat"); let runtime_options = providers::provider_runtime_options_from_config(config); let resolved_provider_name = if provider_name == "deepseek" { config .api_url .as_deref() .map(str::trim) .filter(|url| !url.is_empty()) .map(|url| format!("custom:{url}")) .unwrap_or_else(|| provider_name.to_string()) } else { provider_name.to_string() }; let provider = providers::create_routed_provider_with_options( &resolved_provider_name, config.api_key.as_deref(), config.api_url.as_deref(), &config.reliability, &config.model_routes, model_name, &runtime_options, ) .map_err(map_anyhow_to_pipe_error)?; Ok(Box::new(NonStreamingProvider::new(provider))) } fn map_anyhow_to_pipe_error(err: anyhow::Error) -> PipeError { PipeError::Protocol(err.to_string()) } struct NonStreamingProvider { inner: Box, } impl NonStreamingProvider { fn new(inner: Box) -> Self { Self { inner } } } #[async_trait] impl Provider for NonStreamingProvider { fn capabilities(&self) -> ProviderCapabilities { self.inner.capabilities() } async fn chat_with_system( &self, system_prompt: Option<&str>, message: &str, model: &str, temperature: f64, ) -> anyhow::Result { self.inner .chat_with_system(system_prompt, message, model, temperature) .await } async fn chat_with_history( &self, messages: &[ChatMessage], model: &str, temperature: f64, ) -> anyhow::Result { self.inner.chat_with_history(messages, model, temperature).await } async fn chat( &self, request: ChatRequest<'_>, model: &str, temperature: f64, ) -> anyhow::Result { self.inner.chat(request, model, temperature).await } fn supports_streaming(&self) -> bool { false } fn supports_streaming_tool_events(&self) -> bool { false } fn stream_chat( &self, _request: ChatRequest<'_>, _model: &str, _temperature: f64, _options: StreamOptions, ) -> stream::BoxStream<'static, StreamResult> { stream::empty().boxed() } } fn build_seed_history(task_context: &CompatTaskContext) -> Vec { task_context .messages .iter() .filter_map(to_chat_message) .collect() } fn to_chat_message(message: &ConversationMessage) -> Option { let content = message.content.trim(); if content.is_empty() { return None; } match message.role.as_str() { "user" => Some(ChatMessage::user(content)), "assistant" => Some(ChatMessage::assistant(content)), "system" => Some(ChatMessage::system(content)), _ => None, } }