feat: add phase1 task planner flow

This commit is contained in:
zyl
2026-03-25 03:29:55 +00:00
parent b9773d4719
commit 1ab0012275
5 changed files with 293 additions and 2 deletions

63
src/agent/mod.rs Normal file
View 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
View 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}"),
},
],
})
}

View File

@@ -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(()),