feat: add phase1 task planner flow
This commit is contained in:
63
src/agent/mod.rs
Normal file
63
src/agent/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
pub mod planner;
|
||||
|
||||
use crate::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool, PipeError, Transport};
|
||||
|
||||
pub fn execute_task<T: Transport>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
instruction: &str,
|
||||
) -> Result<String, PipeError> {
|
||||
let plan = planner::plan_instruction(instruction)
|
||||
.map_err(|err| PipeError::Protocol(err.to_string()))?;
|
||||
|
||||
for step in &plan.steps {
|
||||
transport.send(&AgentMessage::LogEntry {
|
||||
level: "info".to_string(),
|
||||
message: step.log_message.clone(),
|
||||
})?;
|
||||
|
||||
let result = browser_tool.invoke(
|
||||
step.action.clone(),
|
||||
step.params.clone(),
|
||||
&step.expected_domain,
|
||||
)?;
|
||||
if !result.success {
|
||||
return Err(PipeError::Protocol(format!(
|
||||
"browser action failed: {}",
|
||||
result.data
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plan.summary)
|
||||
}
|
||||
|
||||
pub fn handle_browser_message<T: Transport>(
|
||||
transport: &T,
|
||||
browser_tool: &BrowserPipeTool<T>,
|
||||
message: BrowserMessage,
|
||||
) -> Result<(), PipeError> {
|
||||
match message {
|
||||
BrowserMessage::SubmitTask { instruction } => {
|
||||
let completion = match execute_task(transport, browser_tool, &instruction) {
|
||||
Ok(summary) => AgentMessage::TaskComplete {
|
||||
success: true,
|
||||
summary,
|
||||
},
|
||||
Err(err) => AgentMessage::TaskComplete {
|
||||
success: false,
|
||||
summary: err.to_string(),
|
||||
},
|
||||
};
|
||||
transport.send(&completion)
|
||||
}
|
||||
BrowserMessage::Init { .. } => {
|
||||
eprintln!("ignoring duplicate init after handshake");
|
||||
Ok(())
|
||||
}
|
||||
BrowserMessage::Response { seq, .. } => {
|
||||
eprintln!("ignoring unsolicited response: seq={seq}");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/agent/planner.rs
Normal file
72
src/agent/planner.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use serde_json::{json, Value};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::pipe::Action;
|
||||
|
||||
const BAIDU_URL: &str = "https://www.baidu.com";
|
||||
const BAIDU_DOMAIN: &str = "www.baidu.com";
|
||||
const BAIDU_INPUT_SELECTOR: &str = "#kw";
|
||||
const BAIDU_SEARCH_BUTTON_SELECTOR: &str = "#su";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PlannedStep {
|
||||
pub action: Action,
|
||||
pub params: Value,
|
||||
pub expected_domain: String,
|
||||
pub log_message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TaskPlan {
|
||||
pub summary: String,
|
||||
pub steps: Vec<PlannedStep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||
pub enum PlannerError {
|
||||
#[error("unsupported instruction: {0}")]
|
||||
UnsupportedInstruction(String),
|
||||
#[error("missing search query in instruction")]
|
||||
MissingQuery,
|
||||
}
|
||||
|
||||
pub fn plan_instruction(instruction: &str) -> Result<TaskPlan, PlannerError> {
|
||||
let trimmed = instruction.trim();
|
||||
let query = trimmed
|
||||
.strip_prefix("打开百度搜索")
|
||||
.or_else(|| trimmed.strip_prefix("打开百度并搜索"))
|
||||
.ok_or_else(|| PlannerError::UnsupportedInstruction(trimmed.to_string()))?
|
||||
.trim();
|
||||
|
||||
if query.is_empty() {
|
||||
return Err(PlannerError::MissingQuery);
|
||||
}
|
||||
|
||||
Ok(TaskPlan {
|
||||
summary: format!("已在百度搜索{query}"),
|
||||
steps: vec![
|
||||
PlannedStep {
|
||||
action: Action::Navigate,
|
||||
params: json!({ "url": BAIDU_URL }),
|
||||
expected_domain: BAIDU_DOMAIN.to_string(),
|
||||
log_message: "navigate https://www.baidu.com".to_string(),
|
||||
},
|
||||
PlannedStep {
|
||||
action: Action::Type,
|
||||
params: json!({
|
||||
"selector": BAIDU_INPUT_SELECTOR,
|
||||
"text": query,
|
||||
"clear_first": true
|
||||
}),
|
||||
expected_domain: BAIDU_DOMAIN.to_string(),
|
||||
log_message: format!("type {query} into {BAIDU_INPUT_SELECTOR}"),
|
||||
},
|
||||
PlannedStep {
|
||||
action: Action::Click,
|
||||
params: json!({ "selector": BAIDU_SEARCH_BUTTON_SELECTOR }),
|
||||
expected_domain: BAIDU_DOMAIN.to_string(),
|
||||
log_message: format!("click {BAIDU_SEARCH_BUTTON_SELECTOR}"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod agent;
|
||||
pub mod pipe;
|
||||
pub mod security;
|
||||
|
||||
@@ -5,6 +6,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agent::handle_browser_message;
|
||||
use pipe::{perform_handshake, BrowserPipeTool, PipeError, StdioTransport, Transport};
|
||||
use security::MacPolicy;
|
||||
|
||||
@@ -19,7 +21,7 @@ pub fn run() -> Result<(), PipeError> {
|
||||
let transport = Arc::new(StdioTransport::new(std::io::stdin(), std::io::stdout()));
|
||||
let handshake = perform_handshake(transport.as_ref(), Duration::from_secs(5))?;
|
||||
let mac_policy = MacPolicy::load_from_path(default_rules_path())?;
|
||||
let _browser_tool = BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)
|
||||
let browser_tool = BrowserPipeTool::new(transport.clone(), mac_policy, handshake.session_key)
|
||||
.with_response_timeout(Duration::from_secs(30));
|
||||
|
||||
eprintln!("sgclaw ready: agent_id={}", handshake.agent_id);
|
||||
@@ -27,7 +29,7 @@ pub fn run() -> Result<(), PipeError> {
|
||||
loop {
|
||||
match transport.recv_timeout(Duration::from_secs(3600)) {
|
||||
Ok(message) => {
|
||||
eprintln!("ignoring unsolicited browser message: {:?}", message);
|
||||
handle_browser_message(transport.as_ref(), &browser_tool, message)?;
|
||||
}
|
||||
Err(PipeError::Timeout) => continue,
|
||||
Err(PipeError::PipeClosed) => return Ok(()),
|
||||
|
||||
Reference in New Issue
Block a user