feat: add websocket browser service runtime
Wire the service/browser runtime onto the websocket-driven execution path and add the new browser/service modules needed for the submit flow and runtime integration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
385
src/agent/task_runner.rs
Normal file
385
src/agent/task_runner.rs
Normal file
@@ -0,0 +1,385 @@
|
||||
use std::ffi::OsString;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::browser::BrowserBackend;
|
||||
use crate::compat::config_adapter::resolve_skills_dir_from_sgclaw_settings;
|
||||
use crate::compat::runtime::CompatTaskContext;
|
||||
use crate::config::SgClawSettings;
|
||||
use crate::pipe::{
|
||||
AgentMessage, BrowserPipeTool, ConversationMessage, PipeError, Transport,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentRuntimeContext {
|
||||
config_path: Option<PathBuf>,
|
||||
workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
impl AgentRuntimeContext {
|
||||
pub fn new(config_path: Option<PathBuf>, workspace_root: PathBuf) -> Self {
|
||||
Self {
|
||||
config_path,
|
||||
workspace_root,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_process_args<I, S>(args: I) -> Result<Self, PipeError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<OsString>,
|
||||
{
|
||||
let mut config_path = None;
|
||||
let mut args = args.into_iter().map(Into::into);
|
||||
let _ = args.next();
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == OsString::from("--config-path") {
|
||||
let Some(value) = args.next() else {
|
||||
return Err(PipeError::Protocol(
|
||||
"missing value for --config-path".to_string(),
|
||||
));
|
||||
};
|
||||
config_path = Some(PathBuf::from(value));
|
||||
continue;
|
||||
}
|
||||
|
||||
let arg_string = arg.to_string_lossy();
|
||||
if let Some(value) = arg_string.strip_prefix("--config-path=") {
|
||||
config_path = Some(PathBuf::from(value));
|
||||
}
|
||||
}
|
||||
|
||||
let workspace_root = config_path
|
||||
.as_ref()
|
||||
.and_then(|path| path.parent().map(|parent| parent.to_path_buf()))
|
||||
.unwrap_or_else(default_workspace_root);
|
||||
|
||||
Ok(Self::new(config_path, workspace_root))
|
||||
}
|
||||
|
||||
pub(crate) fn load_sgclaw_settings(&self) -> Result<Option<SgClawSettings>, PipeError> {
|
||||
SgClawSettings::load(self.config_path.as_deref())
|
||||
.map_err(|err| PipeError::Protocol(err.to_string()))
|
||||
}
|
||||
|
||||
fn settings_source_label(&self) -> String {
|
||||
match &self.config_path {
|
||||
Some(path) if path.exists() => path.display().to_string(),
|
||||
_ => "environment".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentRuntimeContext {
|
||||
fn default() -> Self {
|
||||
Self::new(None, default_workspace_root())
|
||||
}
|
||||
}
|
||||
|
||||
fn default_workspace_root() -> PathBuf {
|
||||
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct SubmitTaskRequest {
|
||||
pub instruction: String,
|
||||
pub conversation_id: Option<String>,
|
||||
pub messages: Vec<ConversationMessage>,
|
||||
pub page_url: Option<String>,
|
||||
pub page_title: Option<String>,
|
||||
}
|
||||
|
||||
pub trait AgentEventSink: Send + Sync {
|
||||
fn send(&self, message: &AgentMessage) -> Result<(), PipeError>;
|
||||
}
|
||||
|
||||
impl<T: Transport + ?Sized> AgentEventSink for T {
|
||||
fn send(&self, message: &AgentMessage) -> Result<(), PipeError> {
|
||||
Transport::send(self, message)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_submit_task<T: Transport + 'static>(
|
||||
transport: &T,
|
||||
sink: &dyn AgentEventSink,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
context: &AgentRuntimeContext,
|
||||
request: SubmitTaskRequest,
|
||||
) -> Result<(), PipeError> {
|
||||
let SubmitTaskRequest {
|
||||
instruction,
|
||||
conversation_id,
|
||||
messages,
|
||||
page_url,
|
||||
page_title,
|
||||
} = request;
|
||||
let instruction = instruction.trim().to_string();
|
||||
if instruction.is_empty() {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: "请输入任务内容。".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let task_context = CompatTaskContext {
|
||||
conversation_id,
|
||||
messages,
|
||||
page_url,
|
||||
page_title,
|
||||
};
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: runtime_version_log_message(),
|
||||
});
|
||||
if !task_context.messages.is_empty() {
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"continuing conversation with {} prior turns",
|
||||
task_context.messages.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let completion = match context.load_sgclaw_settings() {
|
||||
Ok(Some(settings)) => {
|
||||
let resolved_skills_dir =
|
||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"DeepSeek config loaded from {} model={} base_url={}",
|
||||
context.settings_source_label(),
|
||||
settings.provider_model,
|
||||
settings.provider_base_url
|
||||
),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"runtime profile={:?} skills_prompt_mode={:?}",
|
||||
settings.runtime_profile, settings.skills_prompt_mode
|
||||
),
|
||||
});
|
||||
if crate::compat::orchestration::should_use_primary_orchestration(
|
||||
&instruction,
|
||||
task_context.page_url.as_deref(),
|
||||
task_context.page_title.as_deref(),
|
||||
) {
|
||||
let _ = send_mode_log(sink, "zeroclaw_process_message_primary");
|
||||
match crate::compat::orchestration::execute_task_with_sgclaw_settings(
|
||||
transport,
|
||||
browser_tool.clone(),
|
||||
&instruction,
|
||||
&task_context,
|
||||
&context.workspace_root,
|
||||
&settings,
|
||||
) {
|
||||
Ok(summary) => {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: true,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = send_mode_log(sink, "compat_llm_primary");
|
||||
match crate::compat::runtime::execute_task_with_sgclaw_settings(
|
||||
transport,
|
||||
browser_tool.clone(),
|
||||
&instruction,
|
||||
&task_context,
|
||||
&context.workspace_root,
|
||||
&settings,
|
||||
) {
|
||||
Ok(summary) => AgentMessage::TaskComplete {
|
||||
success: true,
|
||||
summary,
|
||||
},
|
||||
Err(err) => AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(None) => AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: missing_llm_configuration_summary(),
|
||||
},
|
||||
Err(err) => {
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "error".to_string(),
|
||||
message: format!("failed to load DeepSeek config: {err}"),
|
||||
});
|
||||
AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sink.send(&completion)
|
||||
}
|
||||
|
||||
pub fn run_submit_task_with_browser_backend<T: Transport + 'static>(
|
||||
_transport: &T,
|
||||
sink: &dyn AgentEventSink,
|
||||
browser_backend: Arc<dyn BrowserBackend>,
|
||||
context: &AgentRuntimeContext,
|
||||
request: SubmitTaskRequest,
|
||||
) -> Result<(), PipeError> {
|
||||
let SubmitTaskRequest {
|
||||
instruction,
|
||||
conversation_id,
|
||||
messages,
|
||||
page_url,
|
||||
page_title,
|
||||
} = request;
|
||||
let instruction = instruction.trim().to_string();
|
||||
if instruction.is_empty() {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: "请输入任务内容。".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let task_context = CompatTaskContext {
|
||||
conversation_id,
|
||||
messages,
|
||||
page_url,
|
||||
page_title,
|
||||
};
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: runtime_version_log_message(),
|
||||
});
|
||||
if !task_context.messages.is_empty() {
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"continuing conversation with {} prior turns",
|
||||
task_context.messages.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let completion = match context.load_sgclaw_settings() {
|
||||
Ok(Some(settings)) => {
|
||||
let resolved_skills_dir =
|
||||
resolve_skills_dir_from_sgclaw_settings(&context.workspace_root, &settings);
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"DeepSeek config loaded from {} model={} base_url={}",
|
||||
context.settings_source_label(),
|
||||
settings.provider_model,
|
||||
settings.provider_base_url
|
||||
),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!("skills dir resolved to {}", resolved_skills_dir.display()),
|
||||
});
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: format!(
|
||||
"runtime profile={:?} skills_prompt_mode={:?}",
|
||||
settings.runtime_profile, settings.skills_prompt_mode
|
||||
),
|
||||
});
|
||||
if crate::compat::orchestration::should_use_primary_orchestration(
|
||||
&instruction,
|
||||
task_context.page_url.as_deref(),
|
||||
task_context.page_title.as_deref(),
|
||||
) {
|
||||
let _ = send_mode_log(sink, "zeroclaw_process_message_primary");
|
||||
match crate::compat::orchestration::execute_task_with_browser_backend(
|
||||
sink,
|
||||
browser_backend.clone(),
|
||||
&instruction,
|
||||
&task_context,
|
||||
&context.workspace_root,
|
||||
&settings,
|
||||
) {
|
||||
Ok(summary) => {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: true,
|
||||
summary,
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
return sink.send(&AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = send_mode_log(sink, "compat_llm_primary");
|
||||
match crate::compat::runtime::execute_task_with_browser_backend(
|
||||
sink,
|
||||
browser_backend,
|
||||
&instruction,
|
||||
&task_context,
|
||||
&context.workspace_root,
|
||||
&settings,
|
||||
) {
|
||||
Ok(summary) => AgentMessage::TaskComplete {
|
||||
success: true,
|
||||
summary,
|
||||
},
|
||||
Err(err) => AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(None) => AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: missing_llm_configuration_summary(),
|
||||
},
|
||||
Err(err) => {
|
||||
let _ = sink.send(&AgentMessage::LogEntry {
|
||||
level: "error".to_string(),
|
||||
message: format!("failed to load DeepSeek config: {err}"),
|
||||
});
|
||||
AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sink.send(&completion)
|
||||
}
|
||||
|
||||
fn send_mode_log(sink: &dyn AgentEventSink, mode: &str) -> Result<(), PipeError> {
|
||||
sink.send(&AgentMessage::LogEntry {
|
||||
level: "mode".to_string(),
|
||||
message: mode.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn missing_llm_configuration_summary() -> String {
|
||||
"未配置大语言模型。请先在 sgclaw_config.json 或环境变量中配置 apiKey、baseUrl 与 model。"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn runtime_version_log_message() -> String {
|
||||
format!(
|
||||
"sgclaw runtime version={} protocol={}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
crate::pipe::protocol::PROTOCOL_VERSION
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user