logging: include runtime and skill versions
This commit is contained in:
@@ -94,6 +94,14 @@ fn missing_llm_configuration_summary() -> String {
|
|||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_version_log_message() -> String {
|
||||||
|
format!(
|
||||||
|
"sgclaw runtime version={} protocol={}",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
crate::pipe::protocol::PROTOCOL_VERSION
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_plan<T: Transport>(
|
fn execute_plan<T: Transport>(
|
||||||
transport: &T,
|
transport: &T,
|
||||||
browser_tool: &BrowserPipeTool<T>,
|
browser_tool: &BrowserPipeTool<T>,
|
||||||
@@ -173,6 +181,10 @@ pub fn handle_browser_message_with_context<T: Transport + 'static>(
|
|||||||
page_url: (!page_url.trim().is_empty()).then_some(page_url),
|
page_url: (!page_url.trim().is_empty()).then_some(page_url),
|
||||||
page_title: (!page_title.trim().is_empty()).then_some(page_title),
|
page_title: (!page_title.trim().is_empty()).then_some(page_title),
|
||||||
};
|
};
|
||||||
|
let _ = transport.send(&AgentMessage::LogEntry {
|
||||||
|
level: "info".to_string(),
|
||||||
|
message: runtime_version_log_message(),
|
||||||
|
});
|
||||||
if !task_context.messages.is_empty() {
|
if !task_context.messages.is_empty() {
|
||||||
let _ = transport.send(&AgentMessage::LogEntry {
|
let _ = transport.send(&AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use zeroclaw::agent::TurnEvent;
|
use zeroclaw::agent::TurnEvent;
|
||||||
|
|
||||||
use crate::pipe::AgentMessage;
|
use crate::pipe::AgentMessage;
|
||||||
|
|
||||||
pub fn log_entry_for_turn_event(event: &TurnEvent) -> Option<AgentMessage> {
|
pub fn log_entry_for_turn_event(
|
||||||
|
event: &TurnEvent,
|
||||||
|
skill_versions: &HashMap<String, String>,
|
||||||
|
) -> Option<AgentMessage> {
|
||||||
match event {
|
match event {
|
||||||
TurnEvent::ToolCall { name, args } => Some(AgentMessage::LogEntry {
|
TurnEvent::ToolCall { name, args } => Some(AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
message: format_tool_call(name, args),
|
message: format_tool_call(name, args, skill_versions),
|
||||||
}),
|
}),
|
||||||
TurnEvent::ToolResult { output, .. } if is_tool_error(output) => Some(AgentMessage::LogEntry {
|
TurnEvent::ToolResult { output, .. } if is_tool_error(output) => Some(AgentMessage::LogEntry {
|
||||||
level: "error".to_string(),
|
level: "error".to_string(),
|
||||||
@@ -17,12 +22,19 @@ pub fn log_entry_for_turn_event(event: &TurnEvent) -> Option<AgentMessage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_tool_call(name: &str, args: &Value) -> String {
|
fn format_tool_call(
|
||||||
|
name: &str,
|
||||||
|
args: &Value,
|
||||||
|
skill_versions: &HashMap<String, String>,
|
||||||
|
) -> String {
|
||||||
if name == "read_skill" {
|
if name == "read_skill" {
|
||||||
let skill_name = args
|
let skill_name = args
|
||||||
.get("name")
|
.get("name")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.unwrap_or("<missing-skill>");
|
.unwrap_or("<missing-skill>");
|
||||||
|
if let Some(version) = skill_versions.get(skill_name) {
|
||||||
|
return format!("read_skill {skill_name}@{version}");
|
||||||
|
}
|
||||||
return format!("read_skill {skill_name}");
|
return format!("read_skill {skill_name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -104,11 +105,19 @@ pub async fn execute_task_with_provider<T: Transport + 'static>(
|
|||||||
message,
|
message,
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let loaded_skill_names = engine.loaded_skill_names(&config, &skills_dir);
|
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
||||||
if !loaded_skill_names.is_empty() {
|
let loaded_skill_versions = loaded_skills
|
||||||
|
.iter()
|
||||||
|
.map(|skill| (skill.name.clone(), skill.version.clone()))
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
let loaded_skill_labels = loaded_skills
|
||||||
|
.iter()
|
||||||
|
.map(|skill| format!("{}@{}", skill.name, skill.version))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !loaded_skill_labels.is_empty() {
|
||||||
transport.send(&crate::pipe::AgentMessage::LogEntry {
|
transport.send(&crate::pipe::AgentMessage::LogEntry {
|
||||||
level: "info".to_string(),
|
level: "info".to_string(),
|
||||||
message: format!("loaded skills: {}", loaded_skill_names.join(", ")),
|
message: format!("loaded skills: {}", loaded_skill_labels.join(", ")),
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let mut tools: Vec<Box<dyn zeroclaw::tools::Tool>> = if browser_surface_present {
|
let mut tools: Vec<Box<dyn zeroclaw::tools::Tool>> = if browser_surface_present {
|
||||||
@@ -161,7 +170,7 @@ pub async fn execute_task_with_provider<T: Transport + 'static>(
|
|||||||
let task = tokio::spawn(async move { agent.turn_streamed(&instruction, event_tx).await });
|
let task = tokio::spawn(async move { agent.turn_streamed(&instruction, event_tx).await });
|
||||||
|
|
||||||
while let Some(event) = event_rx.recv().await {
|
while let Some(event) = event_rx.recv().await {
|
||||||
if let Some(log_entry) = log_entry_for_turn_event(&event) {
|
if let Some(log_entry) = log_entry_for_turn_event(&event, &loaded_skill_versions) {
|
||||||
transport.send(&log_entry)?;
|
transport.send(&log_entry)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,12 +161,30 @@ impl RuntimeEngine {
|
|||||||
sections.join("\n\n")
|
sections.join("\n\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn loaded_skills(
|
||||||
|
&self,
|
||||||
|
config: &ZeroClawConfig,
|
||||||
|
skills_dir: &Path,
|
||||||
|
) -> Vec<zeroclaw::skills::Skill> {
|
||||||
|
let mut skills = load_runtime_skills(config, skills_dir);
|
||||||
|
skills.sort_by(|left, right| {
|
||||||
|
left.name
|
||||||
|
.cmp(&right.name)
|
||||||
|
.then(left.version.cmp(&right.version))
|
||||||
|
});
|
||||||
|
skills.dedup_by(|left, right| {
|
||||||
|
left.name == right.name && left.version == right.version
|
||||||
|
});
|
||||||
|
skills
|
||||||
|
}
|
||||||
|
|
||||||
pub fn loaded_skill_names(
|
pub fn loaded_skill_names(
|
||||||
&self,
|
&self,
|
||||||
config: &ZeroClawConfig,
|
config: &ZeroClawConfig,
|
||||||
skills_dir: &Path,
|
skills_dir: &Path,
|
||||||
) -> Vec<String> {
|
) -> Vec<String> {
|
||||||
let mut names = load_runtime_skills(config, skills_dir)
|
let mut names = self
|
||||||
|
.loaded_skills(config, skills_dir)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|skill| skill.name)
|
.map(|skill| skill.name)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -486,7 +486,8 @@ fn handle_browser_message_loads_skills_from_configured_skills_dir() {
|
|||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" && message.contains("loaded skills: configured-zhihu-skill")
|
if level == "info" &&
|
||||||
|
message.contains("loaded skills: configured-zhihu-skill@0.1.0")
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
assert_eq!(request_bodies.len(), 1);
|
assert_eq!(request_bodies.len(), 1);
|
||||||
@@ -633,6 +634,14 @@ fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_ll
|
|||||||
if *success && summary == "已在百度搜索天气"
|
if *success && summary == "已在百度搜索天气"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
assert!(sent.iter().any(|message| {
|
||||||
|
matches!(
|
||||||
|
message,
|
||||||
|
AgentMessage::LogEntry { level, message }
|
||||||
|
if level == "info" &&
|
||||||
|
message == "sgclaw runtime version=0.1.0 protocol=1.0"
|
||||||
|
)
|
||||||
|
}));
|
||||||
assert!(sent.iter().any(|message| {
|
assert!(sent.iter().any(|message| {
|
||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
@@ -1548,7 +1557,8 @@ fn compat_runtime_logs_read_skill_usage_with_skill_name() {
|
|||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" && message == "read_skill workspace-zhihu-skill"
|
if level == "info" &&
|
||||||
|
message == "read_skill workspace-zhihu-skill@0.1.0"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1617,7 +1627,8 @@ fn handle_browser_message_exposes_real_zhihu_skill_lib_to_provider_request() {
|
|||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" &&
|
if level == "info" &&
|
||||||
message == "loaded skills: office-export-xlsx, zhihu-hotlist, zhihu-hotlist-screen, zhihu-navigate, zhihu-write"
|
message ==
|
||||||
|
"loaded skills: office-export-xlsx@0.1.0, zhihu-hotlist@0.1.0, zhihu-hotlist-screen@0.1.0, zhihu-navigate@0.1.0, zhihu-write@0.1.0"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
assert_eq!(request_bodies.len(), 1);
|
assert_eq!(request_bodies.len(), 1);
|
||||||
@@ -1805,7 +1816,7 @@ fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() {
|
|||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" && message == "read_skill zhihu-hotlist"
|
if level == "info" && message == "read_skill zhihu-hotlist@0.1.0"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
assert!(sent.iter().any(|message| {
|
assert!(sent.iter().any(|message| {
|
||||||
@@ -2741,7 +2752,7 @@ fn handle_browser_message_executes_real_zhihu_navigate_skill_flow() {
|
|||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" && message == "read_skill zhihu-navigate"
|
if level == "info" && message == "read_skill zhihu-navigate@0.1.0"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
assert!(sent.iter().any(|message| {
|
assert!(sent.iter().any(|message| {
|
||||||
@@ -2906,7 +2917,7 @@ fn handle_browser_message_executes_real_zhihu_write_skill_flow() {
|
|||||||
matches!(
|
matches!(
|
||||||
message,
|
message,
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "info" && message == "read_skill zhihu-write"
|
if level == "info" && message == "read_skill zhihu-write@0.1.0"
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
assert!(sent.iter().any(|message| {
|
assert!(sent.iter().any(|message| {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use common::MockTransport;
|
use common::MockTransport;
|
||||||
use sgclaw::agent::handle_browser_message;
|
use sgclaw::agent::handle_browser_message;
|
||||||
use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing};
|
use sgclaw::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool};
|
||||||
use sgclaw::security::MacPolicy;
|
use sgclaw::security::MacPolicy;
|
||||||
|
|
||||||
fn test_policy() -> MacPolicy {
|
fn test_policy() -> MacPolicy {
|
||||||
@@ -23,39 +23,8 @@ fn test_policy() -> MacPolicy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn submit_task_sends_three_commands_and_finishes_with_task_complete() {
|
fn submit_task_without_llm_configuration_returns_clear_error() {
|
||||||
let transport = Arc::new(MockTransport::new(vec![
|
let transport = Arc::new(MockTransport::new(vec![]));
|
||||||
BrowserMessage::Response {
|
|
||||||
seq: 1,
|
|
||||||
success: true,
|
|
||||||
data: serde_json::json!({ "navigated": true }),
|
|
||||||
aom_snapshot: vec![],
|
|
||||||
timing: Timing {
|
|
||||||
queue_ms: 1,
|
|
||||||
exec_ms: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BrowserMessage::Response {
|
|
||||||
seq: 2,
|
|
||||||
success: true,
|
|
||||||
data: serde_json::json!({ "typed": true }),
|
|
||||||
aom_snapshot: vec![],
|
|
||||||
timing: Timing {
|
|
||||||
queue_ms: 1,
|
|
||||||
exec_ms: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
BrowserMessage::Response {
|
|
||||||
seq: 3,
|
|
||||||
success: true,
|
|
||||||
data: serde_json::json!({ "clicked": true }),
|
|
||||||
aom_snapshot: vec![],
|
|
||||||
timing: Timing {
|
|
||||||
queue_ms: 1,
|
|
||||||
exec_ms: 20,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]));
|
|
||||||
let tool = BrowserPipeTool::new(
|
let tool = BrowserPipeTool::new(
|
||||||
transport.clone(),
|
transport.clone(),
|
||||||
test_policy(),
|
test_policy(),
|
||||||
@@ -78,45 +47,15 @@ fn submit_task_sends_three_commands_and_finishes_with_task_complete() {
|
|||||||
|
|
||||||
let sent = transport.sent_messages();
|
let sent = transport.sent_messages();
|
||||||
|
|
||||||
assert_eq!(sent.len(), 8);
|
assert_eq!(sent.len(), 2);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
&sent[0],
|
&sent[0],
|
||||||
AgentMessage::LogEntry { level, message }
|
AgentMessage::LogEntry { level, message }
|
||||||
if level == "mode" && message == "deterministic_planner"
|
if level == "info" && message == "sgclaw runtime version=0.1.0 protocol=1.0"
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
&sent[1],
|
&sent[1],
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message == "navigate https://www.baidu.com"
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[2],
|
|
||||||
AgentMessage::Command { seq, action, .. }
|
|
||||||
if *seq == 1 && action == &Action::Navigate
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[3],
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message == "type 天气 into #kw"
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[4],
|
|
||||||
AgentMessage::Command { seq, action, .. }
|
|
||||||
if *seq == 2 && action == &Action::Type
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[5],
|
|
||||||
AgentMessage::LogEntry { level, message }
|
|
||||||
if level == "info" && message == "click #su"
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[6],
|
|
||||||
AgentMessage::Command { seq, action, .. }
|
|
||||||
if *seq == 3 && action == &Action::Click
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
&sent[7],
|
|
||||||
AgentMessage::TaskComplete { success, summary }
|
AgentMessage::TaskComplete { success, summary }
|
||||||
if *success && summary == "已在百度搜索天气"
|
if !success && summary.contains("未配置大语言模型")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user