sgclaw: move runtime policy into config

This commit is contained in:
zyl
2026-03-29 22:38:20 +08:00
parent 54049a1e1e
commit 7d9036b2d4
9 changed files with 1095 additions and 93 deletions

View File

@@ -1,16 +1,21 @@
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use zeroclaw::Config as ZeroClawConfig;
use zeroclaw::config::schema::ModelProviderConfig;
use crate::compat::cron_adapter::configure_embedded_cron;
use crate::compat::memory_adapter::configure_embedded_memory;
use crate::config::DeepSeekSettings;
use crate::config::{BrowserBackend, DeepSeekSettings, SgClawSettings};
use crate::runtime::RuntimeProfile;
const SGCLAW_ZEROCLAW_WORKSPACE_DIR: &str = ".sgclaw-zeroclaw-workspace";
const SKILLS_DIR_NAME: &str = "skills";
pub fn build_zeroclaw_config(workspace_root: &Path) -> Result<ZeroClawConfig, crate::config::ConfigError> {
let settings = DeepSeekSettings::from_env()?;
Ok(build_zeroclaw_config_from_settings(
let settings = SgClawSettings::from_env()?;
Ok(build_zeroclaw_config_from_sgclaw_settings(
workspace_root,
&settings,
))
@@ -19,15 +24,54 @@ pub fn build_zeroclaw_config(workspace_root: &Path) -> Result<ZeroClawConfig, cr
pub fn build_zeroclaw_config_from_settings(
workspace_root: &Path,
settings: &DeepSeekSettings,
) -> ZeroClawConfig {
build_zeroclaw_config_from_sgclaw_settings(workspace_root, &SgClawSettings::from(settings))
}
pub fn build_zeroclaw_config_from_sgclaw_settings(
workspace_root: &Path,
settings: &SgClawSettings,
) -> ZeroClawConfig {
let workspace_dir = zeroclaw_workspace_dir(workspace_root);
let active_provider = settings.active_provider_settings();
let mut config = ZeroClawConfig::default();
config.workspace_dir = workspace_dir.clone();
config.config_path = workspace_dir.join("config.toml");
config.default_provider = Some("deepseek".to_string());
config.default_model = Some(settings.model.clone());
config.api_key = Some(settings.api_key.clone());
config.api_url = Some(settings.base_url.clone());
config.default_provider = Some(active_provider.provider.clone());
config.default_model = Some(active_provider.model.clone());
config.api_key = Some(active_provider.api_key.clone());
config.api_url = active_provider.base_url.clone();
config.api_path = active_provider.api_path.clone();
config.default_temperature = match settings.runtime_profile {
RuntimeProfile::BrowserAttached | RuntimeProfile::BrowserHeavy => 0.0,
RuntimeProfile::GeneralAssistant => config.default_temperature,
};
config.skills.prompt_injection_mode = settings.skills_prompt_mode;
config.model_providers = settings
.providers
.iter()
.map(|provider| {
(
provider.id.clone(),
ModelProviderConfig {
name: (!provider.provider.starts_with("custom:"))
.then(|| provider.provider.clone()),
base_url: provider.base_url.clone(),
api_path: provider.api_path.clone(),
wire_api: provider.wire_api.clone(),
requires_openai_auth: provider.requires_openai_auth,
azure_openai_resource: None,
azure_openai_deployment: None,
azure_openai_api_version: None,
max_tokens: None,
},
)
})
.collect::<HashMap<_, _>>();
config.browser.enabled = !matches!(settings.browser_backend, BrowserBackend::SuperRpa);
if let Some(backend) = settings.browser_backend.zeroclaw_backend() {
config.browser.backend = backend.to_string();
}
configure_embedded_memory(&mut config);
configure_embedded_cron(&mut config);
config
@@ -36,3 +80,37 @@ pub fn build_zeroclaw_config_from_settings(
pub fn zeroclaw_workspace_dir(workspace_root: &Path) -> PathBuf {
workspace_root.join(SGCLAW_ZEROCLAW_WORKSPACE_DIR)
}
pub fn zeroclaw_default_skills_dir(workspace_root: &Path) -> PathBuf {
zeroclaw_workspace_dir(workspace_root).join(SKILLS_DIR_NAME)
}
pub fn resolve_skills_dir(workspace_root: &Path, settings: &DeepSeekSettings) -> PathBuf {
resolve_skills_dir_path(workspace_root, settings.skills_dir.as_deref())
}
pub fn resolve_skills_dir_from_sgclaw_settings(
workspace_root: &Path,
settings: &SgClawSettings,
) -> PathBuf {
resolve_skills_dir_path(workspace_root, settings.skills_dir.as_deref())
}
fn normalize_configured_skills_dir(configured_dir: &Path) -> PathBuf {
if configured_dir.file_name() == Some(OsStr::new(SKILLS_DIR_NAME)) {
return configured_dir.to_path_buf();
}
let nested_skills_dir = configured_dir.join(SKILLS_DIR_NAME);
if nested_skills_dir.is_dir() {
nested_skills_dir
} else {
configured_dir.to_path_buf()
}
}
fn resolve_skills_dir_path(workspace_root: &Path, configured_dir: Option<&Path>) -> PathBuf {
configured_dir
.map(normalize_configured_skills_dir)
.unwrap_or_else(|| zeroclaw_default_skills_dir(workspace_root))
}

View File

@@ -1,12 +1,9 @@
use std::path::Path;
use std::sync::Arc;
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use futures_util::{stream, StreamExt};
use zeroclaw::agent::dispatcher::NativeToolDispatcher;
use zeroclaw::agent::{Agent, TurnEvent};
use zeroclaw::agent::TurnEvent;
use zeroclaw::config::Config as ZeroClawConfig;
use zeroclaw::observability::{NoopObserver, Observer};
use zeroclaw::providers::{
self, ChatMessage, ChatRequest, ChatResponse, Provider,
};
@@ -14,12 +11,17 @@ use zeroclaw::providers::traits::{
ProviderCapabilities, StreamEvent, StreamOptions, StreamResult,
};
use crate::compat::browser_tool_adapter::{ZeroClawBrowserTool, BROWSER_ACTION_TOOL_NAME};
use crate::compat::config_adapter::build_zeroclaw_config_from_settings;
use crate::config::DeepSeekSettings;
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::compat::memory_adapter::build_memory;
use crate::pipe::{BrowserPipeTool, ConversationMessage, PipeError, Transport};
use crate::runtime::RuntimeEngine;
#[derive(Debug, Clone, Default)]
pub struct CompatTaskContext {
@@ -37,7 +39,27 @@ pub fn execute_task<T: Transport + 'static>(
workspace_root: &Path,
settings: &DeepSeekSettings,
) -> Result<String, PipeError> {
let config = build_zeroclaw_config_from_settings(workspace_root, settings);
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<T: Transport + 'static>(
transport: &T,
browser_tool: BrowserPipeTool<T>,
instruction: &str,
task_context: &CompatTaskContext,
workspace_root: &Path,
settings: &SgClawSettings,
) -> Result<String, PipeError> {
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}")))?;
@@ -49,6 +71,8 @@ pub fn execute_task<T: Transport + 'static>(
instruction,
task_context,
config,
skills_dir,
settings.clone(),
))
}
@@ -59,8 +83,58 @@ pub async fn execute_task_with_provider<T: Transport + 'static>(
instruction: &str,
task_context: &CompatTaskContext,
config: ZeroClawConfig,
skills_dir: PathBuf,
settings: SgClawSettings,
) -> Result<String, PipeError> {
let mut agent = build_agent(browser_tool, provider, &config)?;
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_skill_names = engine.loaded_skill_names(&config, &skills_dir);
if !loaded_skill_names.is_empty() {
transport.send(&crate::pipe::AgentMessage::LogEntry {
level: "info".to_string(),
message: format!("loaded skills: {}", loaded_skill_names.join(", ")),
})?;
}
let mut tools: Vec<Box<dyn zeroclaw::tools::Tool>> = if browser_surface_present {
vec![
Box::new(ZeroClawBrowserTool::new_superrpa(browser_tool.clone())),
Box::new(ZeroClawBrowserTool::new(browser_tool)),
]
} else {
Vec::new()
};
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()
@@ -70,13 +144,19 @@ pub async fn execute_task_with_provider<T: Transport + 'static>(
agent.set_memory_session_id(Some(conversation_id.to_string()));
}
let seed_messages = build_seed_history(task_context);
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::<TurnEvent>(32);
let instruction = instruction.to_string();
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 });
@@ -91,36 +171,6 @@ pub async fn execute_task_with_provider<T: Transport + 'static>(
.map_err(|err| PipeError::Protocol(err.to_string()))
}
fn build_agent<T: Transport + 'static>(
browser_tool: BrowserPipeTool<T>,
provider: Box<dyn Provider>,
config: &ZeroClawConfig,
) -> Result<Agent, PipeError> {
let memory = build_memory(config).map_err(map_anyhow_to_pipe_error)?;
let observer: Arc<dyn Observer> = Arc::new(NoopObserver);
let tools: Vec<Box<dyn zeroclaw::tools::Tool>> =
vec![Box::new(ZeroClawBrowserTool::new(browser_tool))];
Agent::builder()
.provider(provider)
.tools(tools)
.memory(Arc::from(memory))
.observer(observer)
.tool_dispatcher(Box::new(NativeToolDispatcher))
.config(config.agent.clone())
.model_name(
config
.default_model
.clone()
.unwrap_or_else(|| "deepseek-chat".to_string()),
)
.temperature(config.default_temperature)
.workspace_dir(config.workspace_dir.clone())
.allowed_tools(Some(vec![BROWSER_ACTION_TOOL_NAME.to_string()]))
.build()
.map_err(map_anyhow_to_pipe_error)
}
fn build_provider(config: &ZeroClawConfig) -> Result<Box<dyn Provider>, PipeError> {
let provider_name = config.default_provider.as_deref().unwrap_or("deepseek");
let model_name = config