Files
claw/src/agent/task_runner.rs
木炎 96c3bf1dee feat: route staged scene skills through runtime
Add registry-driven scene routing and multi-root skill loading so fault-details and 95598 scene skills can be triggered from natural language while still running through the browser-backed runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:17:17 +08:00

422 lines
14 KiB
Rust

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,
};
use crate::runtime::RuntimeEngine;
#[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(resolve_process_path(PathBuf::from(value)));
continue;
}
let arg_string = arg.to_string_lossy();
if let Some(value) = arg_string.strip_prefix("--config-path=") {
config_path = Some(resolve_process_path(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("."))
}
fn resolve_process_path(path: PathBuf) -> PathBuf {
if path.is_absolute() {
path
} else {
default_workspace_root().join(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_process_args_resolves_relative_config_path_against_current_dir() {
let current_dir = std::env::current_dir().unwrap();
let context = AgentRuntimeContext::from_process_args([
OsString::from("sg_claw"),
OsString::from("--config-path"),
OsString::from("../tmp/sgclaw_config.json"),
])
.unwrap();
assert_eq!(
context.config_path,
Some(current_dir.join("../tmp/sgclaw_config.json"))
);
assert_eq!(context.workspace_root, current_dir.join("../tmp"));
assert!(context.workspace_root.is_absolute());
}
}
#[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_dirs =
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 dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
});
let _ = sink.send(&AgentMessage::LogEntry {
level: "info".to_string(),
message: format!(
"runtime profile={:?} skills_prompt_mode={:?}",
settings.runtime_profile, settings.skills_prompt_mode
),
});
if RuntimeEngine::new(settings.runtime_profile).browser_surface_enabled()
&& 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_dirs =
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 dirs resolved to [{}]", resolved_skills_dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(", ")),
});
let _ = sink.send(&AgentMessage::LogEntry {
level: "info".to_string(),
message: format!(
"runtime profile={:?} skills_prompt_mode={:?}",
settings.runtime_profile, settings.skills_prompt_mode
),
});
if RuntimeEngine::new(settings.runtime_profile).browser_surface_enabled()
&& 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
)
}