From f7e2ff256e5f032f730b44005b326b7166f090ce Mon Sep 17 00:00:00 2001 From: zyl Date: Mon, 30 Mar 2026 00:31:08 +0800 Subject: [PATCH] logging: include runtime and skill versions --- src/agent/mod.rs | 12 ++++++ src/compat/event_bridge.rs | 18 ++++++-- src/compat/runtime.rs | 17 ++++++-- src/runtime/engine.rs | 20 ++++++++- tests/compat_runtime_test.rs | 23 ++++++++--- tests/runtime_task_flow_test.rs | 73 +++------------------------------ 6 files changed, 82 insertions(+), 81 deletions(-) diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 0078972..cb1e9a9 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -94,6 +94,14 @@ fn missing_llm_configuration_summary() -> 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( transport: &T, browser_tool: &BrowserPipeTool, @@ -173,6 +181,10 @@ pub fn handle_browser_message_with_context( page_url: (!page_url.trim().is_empty()).then_some(page_url), 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() { let _ = transport.send(&AgentMessage::LogEntry { level: "info".to_string(), diff --git a/src/compat/event_bridge.rs b/src/compat/event_bridge.rs index 08d3ee1..aa77f3e 100644 --- a/src/compat/event_bridge.rs +++ b/src/compat/event_bridge.rs @@ -1,13 +1,18 @@ +use std::collections::HashMap; + use serde_json::Value; use zeroclaw::agent::TurnEvent; use crate::pipe::AgentMessage; -pub fn log_entry_for_turn_event(event: &TurnEvent) -> Option { +pub fn log_entry_for_turn_event( + event: &TurnEvent, + skill_versions: &HashMap, +) -> Option { match event { TurnEvent::ToolCall { name, args } => Some(AgentMessage::LogEntry { 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 { level: "error".to_string(), @@ -17,12 +22,19 @@ pub fn log_entry_for_turn_event(event: &TurnEvent) -> Option { } } -fn format_tool_call(name: &str, args: &Value) -> String { +fn format_tool_call( + name: &str, + args: &Value, + skill_versions: &HashMap, +) -> String { if name == "read_skill" { let skill_name = args .get("name") .and_then(Value::as_str) .unwrap_or(""); + if let Some(version) = skill_versions.get(skill_name) { + return format!("read_skill {skill_name}@{version}"); + } return format!("read_skill {skill_name}"); } diff --git a/src/compat/runtime.rs b/src/compat/runtime.rs index 48c9968..c2afdb4 100644 --- a/src/compat/runtime.rs +++ b/src/compat/runtime.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::{Path, PathBuf}; use async_trait::async_trait; @@ -104,11 +105,19 @@ pub async fn execute_task_with_provider( message, })?; } - let loaded_skill_names = engine.loaded_skill_names(&config, &skills_dir); - if !loaded_skill_names.is_empty() { + let loaded_skills = engine.loaded_skills(&config, &skills_dir); + let loaded_skill_versions = loaded_skills + .iter() + .map(|skill| (skill.name.clone(), skill.version.clone())) + .collect::>(); + let loaded_skill_labels = loaded_skills + .iter() + .map(|skill| format!("{}@{}", skill.name, skill.version)) + .collect::>(); + if !loaded_skill_labels.is_empty() { transport.send(&crate::pipe::AgentMessage::LogEntry { level: "info".to_string(), - message: format!("loaded skills: {}", loaded_skill_names.join(", ")), + message: format!("loaded skills: {}", loaded_skill_labels.join(", ")), })?; } let mut tools: Vec> = if browser_surface_present { @@ -161,7 +170,7 @@ pub async fn execute_task_with_provider( let task = tokio::spawn(async move { agent.turn_streamed(&instruction, event_tx).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)?; } } diff --git a/src/runtime/engine.rs b/src/runtime/engine.rs index f8781dd..1007789 100644 --- a/src/runtime/engine.rs +++ b/src/runtime/engine.rs @@ -161,12 +161,30 @@ impl RuntimeEngine { sections.join("\n\n") } + pub fn loaded_skills( + &self, + config: &ZeroClawConfig, + skills_dir: &Path, + ) -> Vec { + 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( &self, config: &ZeroClawConfig, skills_dir: &Path, ) -> Vec { - let mut names = load_runtime_skills(config, skills_dir) + let mut names = self + .loaded_skills(config, skills_dir) .into_iter() .map(|skill| skill.name) .collect::>(); diff --git a/tests/compat_runtime_test.rs b/tests/compat_runtime_test.rs index 8ae1aa5..ac3f4ae 100644 --- a/tests/compat_runtime_test.rs +++ b/tests/compat_runtime_test.rs @@ -486,7 +486,8 @@ fn handle_browser_message_loads_skills_from_configured_skills_dir() { matches!( 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); @@ -633,6 +634,14 @@ fn handle_browser_message_routes_supported_instruction_to_compat_runtime_when_ll 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| { matches!( message, @@ -1548,7 +1557,8 @@ fn compat_runtime_logs_read_skill_usage_with_skill_name() { matches!( 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, AgentMessage::LogEntry { level, message } 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); @@ -1805,7 +1816,7 @@ fn handle_browser_message_executes_real_zhihu_hotlist_skill_flow() { matches!( 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| { @@ -2741,7 +2752,7 @@ fn handle_browser_message_executes_real_zhihu_navigate_skill_flow() { matches!( 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| { @@ -2906,7 +2917,7 @@ fn handle_browser_message_executes_real_zhihu_write_skill_flow() { matches!( 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| { diff --git a/tests/runtime_task_flow_test.rs b/tests/runtime_task_flow_test.rs index 4ccd7e6..15925aa 100644 --- a/tests/runtime_task_flow_test.rs +++ b/tests/runtime_task_flow_test.rs @@ -5,7 +5,7 @@ use std::time::Duration; use common::MockTransport; use sgclaw::agent::handle_browser_message; -use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing}; +use sgclaw::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool}; use sgclaw::security::MacPolicy; fn test_policy() -> MacPolicy { @@ -23,39 +23,8 @@ fn test_policy() -> MacPolicy { } #[test] -fn submit_task_sends_three_commands_and_finishes_with_task_complete() { - 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, - }, - }, - ])); +fn submit_task_without_llm_configuration_returns_clear_error() { + let transport = Arc::new(MockTransport::new(vec![])); let tool = BrowserPipeTool::new( transport.clone(), test_policy(), @@ -78,45 +47,15 @@ fn submit_task_sends_three_commands_and_finishes_with_task_complete() { let sent = transport.sent_messages(); - assert_eq!(sent.len(), 8); + assert_eq!(sent.len(), 2); assert!(matches!( &sent[0], 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!( &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 } - if *success && summary == "已在百度搜索天气" + if !success && summary.contains("未配置大语言模型") )); }