sgclaw: snapshot today's runtime and skill updates

This commit is contained in:
zyl
2026-03-30 15:05:39 +08:00
parent c793bfc6a1
commit f51d6b7659
50 changed files with 3473 additions and 621 deletions

View File

@@ -796,7 +796,8 @@ impl Agent {
let (text, calls) = self.tool_dispatcher.parse_response(&response);
let calls = canonicalize_parsed_tool_calls(&self.tools, calls);
response.tool_calls = canonicalize_provider_tool_calls(&self.tools, response.tool_calls);
response.tool_calls =
canonicalize_provider_tool_calls(&self.tools, response.tool_calls);
if calls.is_empty() {
let final_text = if text.is_empty() {
response.text.unwrap_or_default()
@@ -1065,7 +1066,8 @@ impl Agent {
let (text, calls) = self.tool_dispatcher.parse_response(&response);
let calls = canonicalize_parsed_tool_calls(&self.tools, calls);
response.tool_calls = canonicalize_provider_tool_calls(&self.tools, response.tool_calls);
response.tool_calls =
canonicalize_provider_tool_calls(&self.tools, response.tool_calls);
if calls.is_empty() {
let final_text = if text.is_empty() {
response.text.unwrap_or_default()
@@ -1207,7 +1209,8 @@ fn sanitize_final_text(text: &str) -> String {
}
fn resolve_registered_tool_name(tools: &[Box<dyn Tool>], raw: &str) -> Option<String> {
tools.iter()
tools
.iter()
.find(|tool| {
tool.name() == raw || crate::tools::provider_safe_tool_name(tool.name()) == raw
})
@@ -1218,7 +1221,8 @@ fn canonicalize_parsed_tool_calls(
tools: &[Box<dyn Tool>],
calls: Vec<ParsedToolCall>,
) -> Vec<ParsedToolCall> {
calls.into_iter()
calls
.into_iter()
.map(|mut call| {
if let Some(canonical_name) = resolve_registered_tool_name(tools, &call.name) {
call.name = canonical_name;
@@ -1232,7 +1236,8 @@ fn canonicalize_provider_tool_calls(
tools: &[Box<dyn Tool>],
calls: Vec<crate::providers::ToolCall>,
) -> Vec<crate::providers::ToolCall> {
calls.into_iter()
calls
.into_iter()
.map(|mut call| {
if let Some(canonical_name) = resolve_registered_tool_name(tools, &call.name) {
call.name = canonical_name;
@@ -1656,7 +1661,10 @@ mod tests {
.expect("agent builder should succeed with valid config");
let (event_tx, _event_rx) = tokio::sync::mpsc::channel(8);
let response = agent.turn_streamed("读取知乎热榜前10并导出 excel 文件", event_tx).await.unwrap();
let response = agent
.turn_streamed("读取知乎热榜前10并导出 excel 文件", event_tx)
.await
.unwrap();
assert_eq!(
response,

View File

@@ -71,7 +71,7 @@ pub mod routines;
pub mod runtime;
pub(crate) mod security;
pub(crate) mod service;
pub(crate) mod skills;
pub mod skills;
pub mod sop;
pub mod tools;
pub(crate) mod trust;
@@ -83,6 +83,7 @@ pub mod verifiable_intent;
pub mod plugins;
pub use config::Config;
pub use security::{AutonomyLevel, SecurityPolicy};
/// Gateway management subcommands
#[derive(Subcommand, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]

View File

@@ -1943,9 +1943,8 @@ impl Provider for OpenAiCompatibleProvider {
reasoning_effort: self.reasoning_effort_for_model(model),
tool_stream: self
.tool_stream_for_tools(tools.as_ref().is_some_and(|tools| !tools.is_empty())),
tool_choice: self.tool_choice_for_tools(
tools.as_ref().is_some_and(|tools| !tools.is_empty()),
),
tool_choice: self
.tool_choice_for_tools(tools.as_ref().is_some_and(|tools| !tools.is_empty())),
tools,
max_tokens: self.max_tokens,
};
@@ -2099,9 +2098,8 @@ impl Provider for OpenAiCompatibleProvider {
tool_stream: if options.enabled { Some(true) } else { None },
stream: Some(options.enabled),
tools: tools.clone(),
tool_choice: self.tool_choice_for_tools(
tools.as_ref().is_some_and(|tools| !tools.is_empty()),
),
tool_choice: self
.tool_choice_for_tools(tools.as_ref().is_some_and(|tools| !tools.is_empty())),
max_tokens: self.max_tokens,
})
} else {

View File

@@ -816,12 +816,22 @@ pub fn skills_to_prompt_with_mode(
let registered: Vec<_> = skill
.tools
.iter()
.filter(|t| matches!(t.kind.as_str(), "shell" | "script" | "http" | "browser_script"))
.filter(|t| {
matches!(
t.kind.as_str(),
"shell" | "script" | "http" | "browser_script"
)
})
.collect();
let unregistered: Vec<_> = skill
.tools
.iter()
.filter(|t| !matches!(t.kind.as_str(), "shell" | "script" | "http" | "browser_script"))
.filter(|t| {
!matches!(
t.kind.as_str(),
"shell" | "script" | "http" | "browser_script"
)
})
.collect();
if !registered.is_empty() {

View File

@@ -154,7 +154,9 @@ pub async fn read_skill_bundle(location: &Path) -> std::io::Result<String> {
let Some(skill_root) = location.parent() else {
return Ok(primary);
};
let skill_root = skill_root.canonicalize().unwrap_or_else(|_| skill_root.to_path_buf());
let skill_root = skill_root
.canonicalize()
.unwrap_or_else(|_| skill_root.to_path_buf());
let mut output = primary.clone();
let mut appended = BTreeSet::new();
let mut queued = BTreeSet::new();
@@ -275,16 +277,22 @@ fn extract_reference_paths(content: &str) -> Vec<String> {
}
fn looks_like_relative_reference_path(raw: &str) -> bool {
if raw.is_empty() ||
raw.starts_with('/') ||
raw.starts_with("http://") ||
raw.starts_with("https://") ||
raw.starts_with('#')
if raw.is_empty()
|| raw.starts_with('/')
|| raw.starts_with("http://")
|| raw.starts_with("https://")
|| raw.starts_with('#')
{
return false;
}
let candidate = raw.split('#').next().unwrap_or(raw).split('?').next().unwrap_or(raw);
let candidate = raw
.split('#')
.next()
.unwrap_or(raw)
.split('?')
.next()
.unwrap_or(raw);
let path = Path::new(candidate);
if path
.components()
@@ -418,9 +426,15 @@ description = "Ship safely"
assert!(result.success);
assert!(result.output.contains("# Zhihu Hotlist"));
assert!(result.output.contains("## Referenced File: references/collection-flow.md"));
assert!(result.output.contains("Collect rows from the hotlist first."));
assert!(result.output.contains("## Referenced File: references/data-quality.md"));
assert!(result
.output
.contains("## Referenced File: references/collection-flow.md"));
assert!(result
.output
.contains("Collect rows from the hotlist first."));
assert!(result
.output
.contains("## Referenced File: references/data-quality.md"));
assert!(result.output.contains("Mark partial metrics explicitly."));
}
}