Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Bug 1: ExperienceStore store_experience() 相同 pain_pattern 因确定性 URI 直接覆盖,新 Experience reuse_count=0 重置已有积累。修复为先检查 URI 是否已存在,若存在则合并(保留原 id/created_at,reuse_count+1)。 Bug 2: PromptOnlySkill::execute() 只做纯文本 complete(),75 个 Skill 的 tools 字段是装饰性的。修复为扩展 LlmCompleter 支持 complete_with_tools, SkillContext 新增 tool_definitions,KernelSkillExecutor 从 ToolRegistry 解析 manifest 声明的工具定义传入 LLM function calling。
277 lines
9.8 KiB
Rust
277 lines
9.8 KiB
Rust
//! Adapter types bridging runtime interfaces
|
|
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
use async_trait::async_trait;
|
|
use serde_json::{json, Value};
|
|
|
|
use zclaw_runtime::{LlmDriver, tool::{SkillExecutor, HandExecutor}};
|
|
use zclaw_skills::{SkillRegistry, LlmCompleter, SkillCompletion, SkillToolCall};
|
|
use zclaw_hands::HandRegistry;
|
|
use zclaw_types::{AgentId, Result, ToolDefinition};
|
|
|
|
/// Adapter that bridges `zclaw_runtime::LlmDriver` -> `zclaw_skills::LlmCompleter`
|
|
pub(crate) struct LlmDriverAdapter {
|
|
pub(crate) driver: Arc<dyn LlmDriver>,
|
|
pub(crate) max_tokens: u32,
|
|
pub(crate) temperature: f32,
|
|
}
|
|
|
|
impl LlmCompleter for LlmDriverAdapter {
|
|
fn complete(
|
|
&self,
|
|
prompt: &str,
|
|
) -> Pin<Box<dyn std::future::Future<Output = std::result::Result<String, String>> + Send + '_>> {
|
|
let driver = self.driver.clone();
|
|
let prompt = prompt.to_string();
|
|
Box::pin(async move {
|
|
let request = zclaw_runtime::CompletionRequest {
|
|
messages: vec![zclaw_types::Message::user(prompt)],
|
|
max_tokens: Some(self.max_tokens),
|
|
temperature: Some(self.temperature),
|
|
..Default::default()
|
|
};
|
|
let response = driver.complete(request).await
|
|
.map_err(|e| format!("LLM completion error: {}", e))?;
|
|
// Extract text from content blocks
|
|
let text: String = response.content.iter()
|
|
.filter_map(|block| match block {
|
|
zclaw_runtime::ContentBlock::Text { text } => Some(text.as_str()),
|
|
_ => None,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join("");
|
|
Ok(text)
|
|
})
|
|
}
|
|
|
|
fn complete_with_tools(
|
|
&self,
|
|
prompt: &str,
|
|
system_prompt: Option<&str>,
|
|
tools: Vec<ToolDefinition>,
|
|
) -> Pin<Box<dyn std::future::Future<Output = std::result::Result<SkillCompletion, String>> + Send + '_>> {
|
|
let driver = self.driver.clone();
|
|
let prompt = prompt.to_string();
|
|
let system = system_prompt.map(|s| s.to_string());
|
|
let max_tokens = self.max_tokens;
|
|
let temperature = self.temperature;
|
|
Box::pin(async move {
|
|
let mut messages = Vec::new();
|
|
messages.push(zclaw_types::Message::user(prompt));
|
|
|
|
let request = zclaw_runtime::CompletionRequest {
|
|
model: String::new(),
|
|
system,
|
|
messages,
|
|
tools,
|
|
max_tokens: Some(max_tokens),
|
|
temperature: Some(temperature),
|
|
stop: Vec::new(),
|
|
stream: false,
|
|
thinking_enabled: false,
|
|
reasoning_effort: None,
|
|
plan_mode: false,
|
|
};
|
|
let response = driver.complete(request).await
|
|
.map_err(|e| format!("LLM completion error: {}", e))?;
|
|
|
|
let mut text_parts = Vec::new();
|
|
let mut tool_calls = Vec::new();
|
|
for block in &response.content {
|
|
match block {
|
|
zclaw_runtime::ContentBlock::Text { text } => {
|
|
text_parts.push(text.clone());
|
|
}
|
|
zclaw_runtime::ContentBlock::ToolUse { id, name, input } => {
|
|
tool_calls.push(SkillToolCall {
|
|
id: id.clone(),
|
|
name: name.clone(),
|
|
input: input.clone(),
|
|
});
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
Ok(SkillCompletion {
|
|
text: text_parts.join(""),
|
|
tool_calls,
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Skill executor implementation for Kernel
|
|
pub struct KernelSkillExecutor {
|
|
pub(crate) skills: Arc<SkillRegistry>,
|
|
pub(crate) llm: Arc<dyn LlmCompleter>,
|
|
/// Shared tool registry, updated before each skill execution from the
|
|
/// agent loop's freshly-built registry. Uses std::sync because reads
|
|
/// happen from async code but writes are brief and infrequent.
|
|
pub(crate) tool_registry: std::sync::RwLock<Option<zclaw_runtime::ToolRegistry>>,
|
|
}
|
|
|
|
impl KernelSkillExecutor {
|
|
pub fn new(skills: Arc<SkillRegistry>, driver: Arc<dyn LlmDriver>) -> Self {
|
|
let llm: Arc<dyn LlmCompleter> = Arc::new(LlmDriverAdapter { driver, max_tokens: 4096, temperature: 0.7 });
|
|
Self { skills, llm, tool_registry: std::sync::RwLock::new(None) }
|
|
}
|
|
|
|
/// Update the tool registry snapshot. Called by the kernel before each
|
|
/// agent loop iteration so skill execution sees the latest tool set.
|
|
pub fn set_tool_registry(&self, registry: zclaw_runtime::ToolRegistry) {
|
|
if let Ok(mut guard) = self.tool_registry.write() {
|
|
*guard = Some(registry);
|
|
}
|
|
}
|
|
|
|
/// Resolve the tool definitions declared by a skill manifest against
|
|
/// the currently active tool registry.
|
|
fn resolve_tool_definitions(&self, skill_id: &str) -> Vec<ToolDefinition> {
|
|
let manifests = self.skills.manifests_snapshot();
|
|
let manifest = match manifests.get(&zclaw_types::SkillId::new(skill_id)) {
|
|
Some(m) => m,
|
|
None => return vec![],
|
|
};
|
|
if manifest.tools.is_empty() {
|
|
return vec![];
|
|
}
|
|
let guard = match self.tool_registry.read() {
|
|
Ok(g) => g,
|
|
Err(_) => return vec![],
|
|
};
|
|
let registry = match guard.as_ref() {
|
|
Some(r) => r,
|
|
None => return vec![],
|
|
};
|
|
// Only include definitions for tools declared in the skill manifest.
|
|
registry.definitions().into_iter()
|
|
.filter(|def| manifest.tools.iter().any(|t| t == &def.name))
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl SkillExecutor for KernelSkillExecutor {
|
|
async fn execute_skill(
|
|
&self,
|
|
skill_id: &str,
|
|
agent_id: &str,
|
|
session_id: &str,
|
|
input: Value,
|
|
) -> Result<Value> {
|
|
let tool_definitions = self.resolve_tool_definitions(skill_id);
|
|
let context = zclaw_skills::SkillContext {
|
|
agent_id: agent_id.to_string(),
|
|
session_id: session_id.to_string(),
|
|
llm: Some(self.llm.clone()),
|
|
tool_definitions,
|
|
..Default::default()
|
|
};
|
|
let result = self.skills.execute(&zclaw_types::SkillId::new(skill_id), &context, input).await?;
|
|
Ok(result.output)
|
|
}
|
|
|
|
fn get_skill_detail(&self, skill_id: &str) -> Option<zclaw_runtime::tool::SkillDetail> {
|
|
let manifests = self.skills.manifests_snapshot();
|
|
let manifest = manifests.get(&zclaw_types::SkillId::new(skill_id))?;
|
|
Some(zclaw_runtime::tool::SkillDetail {
|
|
id: manifest.id.as_str().to_string(),
|
|
name: manifest.name.clone(),
|
|
description: manifest.description.clone(),
|
|
category: manifest.category.clone(),
|
|
input_schema: manifest.input_schema.clone(),
|
|
triggers: manifest.triggers.clone(),
|
|
capabilities: manifest.capabilities.clone(),
|
|
})
|
|
}
|
|
|
|
fn list_skill_index(&self) -> Vec<zclaw_runtime::tool::SkillIndexEntry> {
|
|
let manifests = self.skills.manifests_snapshot();
|
|
manifests.values()
|
|
.filter(|m| m.enabled)
|
|
.map(|m| zclaw_runtime::tool::SkillIndexEntry {
|
|
id: m.id.as_str().to_string(),
|
|
description: m.description.clone(),
|
|
triggers: m.triggers.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Inbox wrapper for A2A message receivers that supports re-queuing
|
|
/// non-matching messages instead of dropping them.
|
|
pub(crate) struct AgentInbox {
|
|
pub(crate) rx: tokio::sync::mpsc::Receiver<zclaw_protocols::A2aEnvelope>,
|
|
pub(crate) pending: std::collections::VecDeque<zclaw_protocols::A2aEnvelope>,
|
|
}
|
|
|
|
impl AgentInbox {
|
|
pub(crate) fn new(rx: tokio::sync::mpsc::Receiver<zclaw_protocols::A2aEnvelope>) -> Self {
|
|
Self { rx, pending: std::collections::VecDeque::new() }
|
|
}
|
|
|
|
pub(crate) fn try_recv(&mut self) -> std::result::Result<zclaw_protocols::A2aEnvelope, tokio::sync::mpsc::error::TryRecvError> {
|
|
if let Some(msg) = self.pending.pop_front() {
|
|
return Ok(msg);
|
|
}
|
|
self.rx.try_recv()
|
|
}
|
|
|
|
pub(crate) async fn recv(&mut self) -> Option<zclaw_protocols::A2aEnvelope> {
|
|
if let Some(msg) = self.pending.pop_front() {
|
|
return Some(msg);
|
|
}
|
|
self.rx.recv().await
|
|
}
|
|
|
|
pub(crate) fn requeue(&mut self, envelope: zclaw_protocols::A2aEnvelope) {
|
|
self.pending.push_back(envelope);
|
|
}
|
|
}
|
|
|
|
/// Hand executor implementation for Kernel
|
|
///
|
|
/// Bridges `zclaw_runtime::tool::HandExecutor` → `zclaw_hands::HandRegistry`,
|
|
/// allowing `HandTool::execute()` to dispatch to the real Hand implementations.
|
|
pub struct KernelHandExecutor {
|
|
hands: Arc<HandRegistry>,
|
|
}
|
|
|
|
impl KernelHandExecutor {
|
|
pub fn new(hands: Arc<HandRegistry>) -> Self {
|
|
Self { hands }
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl HandExecutor for KernelHandExecutor {
|
|
async fn execute_hand(
|
|
&self,
|
|
hand_id: &str,
|
|
agent_id: &AgentId,
|
|
input: Value,
|
|
) -> Result<Value> {
|
|
let context = zclaw_hands::HandContext {
|
|
agent_id: agent_id.clone(),
|
|
working_dir: None,
|
|
env: std::collections::HashMap::new(),
|
|
timeout_secs: 300,
|
|
callback_url: None,
|
|
};
|
|
let result = self.hands.execute(hand_id, &context, input).await?;
|
|
if result.success {
|
|
Ok(result.output)
|
|
} else {
|
|
Ok(json!({
|
|
"hand_id": hand_id,
|
|
"status": "failed",
|
|
"error": result.error.unwrap_or_else(|| "Unknown hand execution error".to_string()),
|
|
"output": result.output,
|
|
"duration_ms": result.duration_ms,
|
|
}))
|
|
}
|
|
}
|
|
}
|