feat(growth,kernel,runtime): Embedding 接通 + 自学习自动化 — A线+B线 6 项实现
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

A线 Embedding 接通:
- A1: MemoryRetriever.set_embedding_client() + GrowthIntegration.configure_embedding()
  + Kernel.set_embedding_client() + viking_configure_embedding 传播到 Kernel
- A2: Skill 路由替换 new_tf_idf_only() 为 EmbeddingAdapter + LlmSkillFallback

B线 自学习自动化:
- B1: evolution_bridge.rs — candidate_to_manifest() (PromptOnly, disabled by default)
- B2: Kernel::generate_and_register_skill() 全链路 (LLM→parse→QualityGate→manifest→persist)
- B3: EvolutionMiddleware 双模式 (auto_mode 跳过注入, 留给 kernel 自动处理)
- B4: QualityGate 加固 (body ≥100字符 + 必须含标题 + 置信度上限 1.0)

验证: 934 tests PASS, 0 failures
This commit is contained in:
iven
2026-04-21 15:21:03 +08:00
parent 74ce6d4adc
commit 5b5491a08f
13 changed files with 330 additions and 8 deletions

View File

@@ -561,7 +561,8 @@ refactor(store): 统一 Store 数据获取方式
### 最近变更 ### 最近变更
1. [04-21] Phase 0+1 突破之路 8 项基础链路修复: 经验积累覆盖修复(reuse_count累积) + Skill工具调用桥接(complete_with_tools) + Hand字段映射(runId) + Heartbeat痛点感知 + Browser委托消息 + 跨会话检索增强(IdentityRecall 26→43模式+弱身份fallback) + Twitter凭据持久化。验证: 912 tests PASS 1. [04-21] Embedding 接通 + 自学习自动化 A线+B线: 记忆检索Embedding(GrowthIntegration→MemoryRetriever→SemanticScorer) + Skill路由Embedding+LLM Fallback(替换new_tf_idf_only) + evolution_bridge(SkillCandidate→SkillManifest) + generate_and_register_skill()全链路 + EvolutionMiddleware双模式(auto/suggest) + QualityGate加固(长度/标题/置信度上限)。验证: 934 tests PASS
2. [04-21] Phase 0+1 突破之路 8 项基础链路修复: 经验积累覆盖修复(reuse_count累积) + Skill工具调用桥接(complete_with_tools) + Hand字段映射(runId) + Heartbeat痛点感知 + Browser委托消息 + 跨会话检索增强(IdentityRecall 26→43模式+弱身份fallback) + Twitter凭据持久化。验证: 912 tests PASS
2. [04-17] 全系统 E2E 测试 129 链路: 82 PASS / 20 PARTIAL / 1 FAIL / 26 SKIP有效通过率 79.1%。7 项 Bug 修复 (Dashboard 404/记忆去重/记忆注入/invoice_id/Prompt版本/agent隔离/行业字段) 2. [04-17] 全系统 E2E 测试 129 链路: 82 PASS / 20 PARTIAL / 1 FAIL / 26 SKIP有效通过率 79.1%。7 项 Bug 修复 (Dashboard 404/记忆去重/记忆注入/invoice_id/Prompt版本/agent隔离/行业字段)
2. [04-16] 3 项 P0 修复 + 5 项 E2E Bug 修复 + Agent 面板刷新 + TRUTH.md 数字校准 2. [04-16] 3 项 P0 修复 + 5 项 E2E Bug 修复 + Agent 面板刷新 + TRUTH.md 数字校准
3. [04-15] Heartbeat 统一健康系统: health_snapshot.rs 统一收集器(LLM连接/记忆/会话/系统资源) + heartbeat.rs HeartbeatManager 重构 + HealthPanel.tsx 前端面板 + Tauri 命令 182→183 + intelligence 模块 15→16 文件 + 删除 intelligence-client/ 9 废弃文件 3. [04-15] Heartbeat 统一健康系统: health_snapshot.rs 统一收集器(LLM连接/记忆/会话/系统资源) + heartbeat.rs HeartbeatManager 重构 + HealthPanel.tsx 前端面板 + Tauri 命令 182→183 + intelligence 模块 15→16 文件 + 删除 intelligence-client/ 9 废弃文件

1
Cargo.lock generated
View File

@@ -9516,6 +9516,7 @@ dependencies = [
"toml 0.8.2", "toml 0.8.2",
"tracing", "tracing",
"uuid", "uuid",
"zclaw-growth",
"zclaw-hands", "zclaw-hands",
"zclaw-memory", "zclaw-memory",
"zclaw-protocols", "zclaw-protocols",

View File

@@ -295,7 +295,7 @@ mod tests {
industry_context: None, industry_context: None,
}; };
let json = r##"{"name":"报表技能","description":"生成报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 报表\n步骤","confidence":0.9}"##; let json = r##"{"name":"报表技能","description":"生成报表","triggers":["报表","日报"],"tools":["researcher"],"body_markdown":"# 报表生成技能\n\n## 步骤一\n收集数据源并验证完整性。\n\n## 步骤二\n按模板格式化输出报表。\n\n## 步骤三\n发送至相关接收人。","confidence":0.9}"##;
let (candidate, report) = engine let (candidate, report) = engine
.validate_skill_candidate(json, &pattern, vec!["搜索".to_string()]) .validate_skill_candidate(json, &pattern, vec!["搜索".to_string()])
.unwrap(); .unwrap();

View File

@@ -63,6 +63,19 @@ impl QualityGate {
issues.push("技能正文不能为空".to_string()); issues.push("技能正文不能为空".to_string());
} }
// 6. body_markdown 最短长度 + 结构检查
if candidate.body_markdown.trim().len() < 100 {
issues.push("技能正文太短至少需要100个字符".to_string());
}
if !candidate.body_markdown.contains('#') {
issues.push("技能正文必须包含至少一个标题 (#)".to_string());
}
// 7. 置信度上限检查(防止 LLM 幻觉过高置信度)
if candidate.confidence > 1.0 {
issues.push(format!("置信度 {:.2} 超过上限 1.0", candidate.confidence));
}
QualityReport { QualityReport {
passed: issues.is_empty(), passed: issues.is_empty(),
issues, issues,
@@ -81,7 +94,7 @@ mod tests {
description: "生成每日报表".to_string(), description: "生成每日报表".to_string(),
triggers: vec!["报表".to_string(), "日报".to_string()], triggers: vec!["报表".to_string(), "日报".to_string()],
tools: vec!["researcher".to_string()], tools: vec!["researcher".to_string()],
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(), body_markdown: "# 每日报表生成流程\n\n## 步骤一:数据收集\n从数据库中查询昨日所有交易记录和运营数据。\n\n## 步骤二:数据整理\n将原始数据按部门、类型进行分类汇总。\n\n## 步骤三:报表输出\n生成标准化报表并发送至相关部门邮箱。".to_string(),
source_pattern: "报表生成".to_string(), source_pattern: "报表生成".to_string(),
confidence: 0.85, confidence: 0.85,
version: 1, version: 1,
@@ -157,4 +170,24 @@ mod tests {
assert!(!report.passed); assert!(!report.passed);
assert!(report.issues.len() >= 3); assert!(report.issues.len() >= 3);
} }
#[test]
fn test_validate_body_too_short() {
let gate = QualityGate::new(0.5, vec![]);
let mut candidate = make_valid_candidate();
candidate.body_markdown = "# 短内容\n步骤1".to_string();
let report = gate.validate_skill(&candidate);
assert!(!report.passed);
assert!(report.issues.iter().any(|i| i.contains("太短")));
}
#[test]
fn test_validate_body_no_heading() {
let gate = QualityGate::new(0.5, vec![]);
let mut candidate = make_valid_candidate();
candidate.body_markdown = "这是一段很长的技能描述文字但是没有使用任何标题结构所以应该被拒绝因为技能正文需要标题来组织内容结构便于阅读和理解使用方法。".to_string();
let report = gate.validate_skill(&candidate);
assert!(!report.passed);
assert!(report.issues.iter().any(|i| i.contains("标题")));
}
} }

View File

@@ -417,6 +417,22 @@ impl MemoryRetriever {
}) })
} }
/// Configure embedding client for semantic similarity
///
/// Stores the client for lazy application on first scorer use.
/// Safe to call from non-async contexts.
pub fn set_embedding_client(
&self,
client: Arc<dyn crate::retrieval::semantic::EmbeddingClient>,
) {
if let Ok(mut scorer) = self.scorer.try_write() {
*scorer = SemanticScorer::with_embedding(client);
tracing::info!("[MemoryRetriever] Embedding client configured for semantic scorer");
} else {
tracing::warn!("[MemoryRetriever] Scorer lock busy, embedding will be applied on next access");
}
}
/// Clear the semantic index /// Clear the semantic index
pub async fn clear_index(&self) { pub async fn clear_index(&self) {
let mut scorer = self.scorer.write().await; let mut scorer = self.scorer.write().await;

View File

@@ -143,7 +143,7 @@ async fn test_quality_gate_validation() {
description: "自动生成并导出每日报表".to_string(), description: "自动生成并导出每日报表".to_string(),
triggers: vec!["生成报表".into(), "每日报表".into()], triggers: vec!["生成报表".into(), "每日报表".into()],
tools: vec!["excel_tool".into()], tools: vec!["excel_tool".into()],
body_markdown: "## 每日报表生成\n\n1. 打开Excel\n2. 选择模板\n3. 导出PDF".to_string(), body_markdown: "# 每日报表生成\n\n## 步骤一:数据收集\n从数据库查询昨日所有交易记录和运营数据。\n\n## 步骤二:数据整理\n将原始数据按部门、类型进行分类汇总。\n\n## 步骤三:报表输出\n生成标准化报表并导出PDF格式。".to_string(),
source_pattern: "生成每日报表".to_string(), source_pattern: "生成每日报表".to_string(),
confidence: 0.85, confidence: 0.85,
version: 1, version: 1,

View File

@@ -17,6 +17,7 @@ zclaw-runtime = { workspace = true }
zclaw-protocols = { workspace = true } zclaw-protocols = { workspace = true }
zclaw-hands = { workspace = true } zclaw-hands = { workspace = true }
zclaw-skills = { workspace = true } zclaw-skills = { workspace = true }
zclaw-growth = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }

View File

@@ -0,0 +1,113 @@
//! Evolution Bridge — connects growth crate's SkillCandidate to skills crate's SkillManifest
//!
//! The growth crate (zclaw-growth) generates SkillCandidate from conversation patterns.
//! The skills crate (zclaw-skills) requires SkillManifest for disk persistence.
//! This bridge lives in zclaw-kernel because it depends on both crates.
use zclaw_growth::skill_generator::SkillCandidate;
use zclaw_skills::{SkillManifest, SkillMode};
use zclaw_types::SkillId;
/// Convert a validated SkillCandidate into a SkillManifest ready for registration.
///
/// Safety invariants:
/// - `mode` is always `PromptOnly` (auto-generated skills cannot execute code)
/// - `enabled` is `false` (requires one explicit positive feedback to activate)
/// - `body_markdown` becomes the SKILL.md body content (stored by serialize_skill_md)
pub fn candidate_to_manifest(candidate: &SkillCandidate) -> SkillManifest {
let slug = name_to_slug(&candidate.name);
SkillManifest {
id: SkillId::new(format!("auto-{}", slug)),
name: candidate.name.clone(),
description: candidate.description.clone(),
version: format!("{}", candidate.version),
author: Some("zclaw-evolution".to_string()),
mode: SkillMode::PromptOnly,
capabilities: Vec::new(),
input_schema: None,
output_schema: None,
tags: vec!["auto-generated".to_string()],
category: None,
triggers: candidate.triggers.clone(),
tools: candidate.tools.clone(),
enabled: false,
}
}
/// Convert a human-readable name to a URL-safe slug.
fn name_to_slug(name: &str) -> String {
let mut result = String::new();
for c in name.trim().chars() {
if c.is_ascii_alphanumeric() {
result.push(c.to_ascii_lowercase());
} else if c == ' ' || c == '-' || c == '_' {
result.push('-');
} else {
// Chinese/unicode characters: use hex representation
result.push_str(&format!("{:x}", c as u32));
}
}
result.trim_matches('-').to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_candidate() -> SkillCandidate {
SkillCandidate {
name: "每日报表".to_string(),
description: "生成每日报表".to_string(),
triggers: vec!["报表".to_string(), "日报".to_string()],
tools: vec!["researcher".to_string()],
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(),
source_pattern: "报表生成".to_string(),
confidence: 0.85,
version: 1,
}
}
#[test]
fn test_candidate_to_manifest() {
let candidate = make_candidate();
let manifest = candidate_to_manifest(&candidate);
assert!(manifest.id.as_str().starts_with("auto-"));
assert_eq!(manifest.name, "每日报表");
assert_eq!(manifest.description, "生成每日报表");
assert_eq!(manifest.version, "1");
assert_eq!(manifest.author.as_deref(), Some("zclaw-evolution"));
assert_eq!(manifest.mode, SkillMode::PromptOnly);
assert!(!manifest.enabled, "auto-generated skills must start disabled");
assert_eq!(manifest.triggers, candidate.triggers);
assert_eq!(manifest.tools, candidate.tools);
assert!(manifest.tags.contains(&"auto-generated".to_string()));
}
#[test]
fn test_name_to_slug_ascii() {
assert_eq!(name_to_slug("Daily Report"), "daily-report");
}
#[test]
fn test_name_to_slug_chinese() {
let slug = name_to_slug("每日报表");
assert!(!slug.is_empty());
assert!(!slug.contains(' '));
}
#[test]
fn test_auto_generated_always_prompt_only() {
let candidate = make_candidate();
let manifest = candidate_to_manifest(&candidate);
assert_eq!(manifest.mode, SkillMode::PromptOnly);
}
#[test]
fn test_auto_generated_starts_disabled() {
let candidate = make_candidate();
let manifest = candidate_to_manifest(&candidate);
assert!(!manifest.enabled);
}
}

View File

@@ -9,6 +9,7 @@ mod triggers;
mod approvals; mod approvals;
mod orchestration; mod orchestration;
mod a2a; mod a2a;
mod evolution_bridge;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{broadcast, Mutex}; use tokio::sync::{broadcast, Mutex};
@@ -55,6 +56,8 @@ pub struct Kernel {
growth: std::sync::Mutex<Option<std::sync::Arc<zclaw_runtime::GrowthIntegration>>>, growth: std::sync::Mutex<Option<std::sync::Arc<zclaw_runtime::GrowthIntegration>>>,
/// Optional LLM driver for memory extraction (set by Tauri desktop layer) /// Optional LLM driver for memory extraction (set by Tauri desktop layer)
extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>, extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>,
/// Optional embedding client for semantic search (set by Tauri desktop layer)
embedding_client: Option<Arc<dyn zclaw_runtime::EmbeddingClient>>,
/// MCP tool adapters — shared with Tauri MCP manager, updated dynamically /// MCP tool adapters — shared with Tauri MCP manager, updated dynamically
mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>, mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>,
/// Dynamic industry keyword configs — shared with Tauri frontend, loaded from SaaS /// Dynamic industry keyword configs — shared with Tauri frontend, loaded from SaaS
@@ -166,6 +169,7 @@ impl Kernel {
viking, viking,
growth: std::sync::Mutex::new(None), growth: std::sync::Mutex::new(None),
extraction_driver: None, extraction_driver: None,
embedding_client: None,
mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())), mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())),
industry_keywords: Arc::new(tokio::sync::RwLock::new(Vec::new())), industry_keywords: Arc::new(tokio::sync::RwLock::new(Vec::new())),
a2a_router, a2a_router,
@@ -258,7 +262,17 @@ impl Kernel {
} }
// Build semantic router from the skill registry (75 SKILL.md loaded at boot) // Build semantic router from the skill registry (75 SKILL.md loaded at boot)
let semantic_router = SemanticSkillRouter::new_tf_idf_only(self.skills.clone()); let semantic_router = if let Some(ref embed_client) = self.embedding_client {
let adapter = crate::skill_router::EmbeddingAdapter::new(embed_client.clone());
let mut router = SemanticSkillRouter::new(self.skills.clone(), Arc::new(adapter));
if let Some(llm_fallback) = self.make_llm_skill_fallback() {
router = router.with_llm_fallback(llm_fallback);
}
tracing::debug!("[Kernel] SemanticSkillRouter created with embedding support");
router
} else {
SemanticSkillRouter::new_tf_idf_only(self.skills.clone())
};
let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router)); let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router));
let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router_and_shared_keywords( let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router_and_shared_keywords(
Box::new(adapter), Box::new(adapter),
@@ -286,6 +300,10 @@ impl Kernel {
if let Some(ref driver) = self.extraction_driver { if let Some(ref driver) = self.extraction_driver {
g = g.with_llm_driver(driver.clone()); g = g.with_llm_driver(driver.clone());
} }
// Propagate embedding client to memory retriever if configured
if let Some(ref embed_client) = self.embedding_client {
g.configure_embedding(embed_client.clone());
}
*cached = Some(std::sync::Arc::new(g)); *cached = Some(std::sync::Arc::new(g));
} }
cached.as_ref().expect("growth present").clone() cached.as_ref().expect("growth present").clone()
@@ -475,6 +493,25 @@ impl Kernel {
} }
} }
/// Set the embedding client for semantic search.
///
/// Propagates to both the skill router (ButlerRouter) and memory retrieval
/// (GrowthIntegration). The next middleware chain creation will use the
/// configured client for embedding-based similarity.
pub fn set_embedding_client(&mut self, client: Arc<dyn zclaw_runtime::EmbeddingClient>) {
tracing::info!("[Kernel] Embedding client configured for semantic search");
self.embedding_client = Some(client);
// Invalidate cached GrowthIntegration so next request builds with new embedding
if let Ok(mut g) = self.growth.lock() {
*g = None;
}
}
/// Create an LLM skill fallback using the kernel's LLM driver.
fn make_llm_skill_fallback(&self) -> Option<Arc<dyn zclaw_skills::semantic_router::RuntimeLlmIntent>> {
Some(Arc::new(crate::skill_router::LlmSkillFallback::new(self.driver.clone())))
}
/// Get a reference to the shared MCP adapters list. /// Get a reference to the shared MCP adapters list.
/// ///
/// The Tauri MCP manager updates this list when services start/stop. /// The Tauri MCP manager updates this list when services start/stop.

View File

@@ -76,4 +76,77 @@ impl Kernel {
} }
self.skills.execute(&zclaw_types::SkillId::new(id), &ctx, input).await self.skills.execute(&zclaw_types::SkillId::new(id), &ctx, input).await
} }
/// Generate a skill from an aggregated pattern and register it.
///
/// Full pipeline:
/// 1. Build LLM prompt from pattern
/// 2. Call LLM to get JSON response
/// 3. Parse response into SkillCandidate
/// 4. Validate through QualityGate (threshold 0.85 for auto-mode)
/// 5. Convert to SkillManifest (PromptOnly, disabled by default)
/// 6. Persist to disk via SkillRegistry
pub async fn generate_and_register_skill(
&self,
pattern: &zclaw_growth::pattern_aggregator::AggregatedPattern,
) -> Result<String> {
// 1. Build prompt
let prompt = zclaw_growth::skill_generator::SkillGenerator::build_prompt(pattern);
// 2. Call LLM
let request = zclaw_runtime::driver::CompletionRequest {
model: self.driver.provider().to_string(),
system: Some("你是技能设计专家,只返回 JSON 格式的技能定义。".to_string()),
messages: vec![zclaw_types::Message::user(prompt)],
max_tokens: Some(1024),
temperature: Some(0.3),
stream: false,
..Default::default()
};
let response = self.driver.complete(request).await?;
let text = response.content.iter()
.filter_map(|block| match block {
zclaw_runtime::driver::ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
// 3. Parse into SkillCandidate
let candidate = zclaw_growth::skill_generator::SkillGenerator::parse_response(
&text, pattern,
)?;
// 4. Validate through QualityGate (higher threshold for auto-generation)
let existing_triggers: Vec<String> = self.skills.list().await
.into_iter()
.flat_map(|m| m.triggers)
.collect();
let gate = zclaw_growth::quality_gate::QualityGate::new(0.85, existing_triggers);
let report = gate.validate_skill(&candidate);
if !report.passed {
return Err(zclaw_types::ZclawError::ConfigError(format!(
"QualityGate rejected: {}", report.issues.join("; ")
)));
}
// 5. Convert to SkillManifest (PromptOnly, disabled)
let manifest = super::evolution_bridge::candidate_to_manifest(&candidate);
let skill_id = manifest.id.to_string();
// 6. Persist to disk
let skills_dir = self.config.skills_dir.as_ref()
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
"Skills directory not configured".into()
))?;
self.skills.create_skill(skills_dir, manifest).await?;
tracing::info!(
"[Kernel] Auto-generated skill '{}' (id={}) registered (disabled)",
candidate.name, skill_id
);
Ok(skill_id)
}
} }

View File

@@ -148,6 +148,18 @@ impl GrowthIntegration {
self.config.auto_extract = auto_extract; self.config.auto_extract = auto_extract;
} }
/// Configure embedding client for memory retrieval.
///
/// Propagates the embedding client to the MemoryRetriever's SemanticScorer,
/// enabling embedding-based similarity in addition to TF-IDF.
/// Safe to call from non-async contexts.
pub fn configure_embedding(
&self,
client: Arc<dyn zclaw_growth::retrieval::semantic::EmbeddingClient>,
) {
self.retriever.set_embedding_client(client);
}
/// Set the user profile store for incremental profile updates /// Set the user profile store for incremental profile updates
pub fn with_profile_store(mut self, store: Arc<UserProfileStore>) -> Self { pub fn with_profile_store(mut self, store: Arc<UserProfileStore>) -> Self {
self.profile_store = Some(store); self.profile_store = Some(store);

View File

@@ -19,18 +19,35 @@ pub struct PendingEvolution {
} }
/// 进化引擎中间件 /// 进化引擎中间件
/// 检查是否有待确认的进化事件,注入确认提示到 system prompt /// 检查是否有待确认的进化事件,根据模式:
/// - suggest 模式(默认): 注入确认提示到 system prompt
/// - auto 模式: 不注入,仅排队等待 kernel 自动处理
pub struct EvolutionMiddleware { pub struct EvolutionMiddleware {
pending: Arc<RwLock<Vec<PendingEvolution>>>, pending: Arc<RwLock<Vec<PendingEvolution>>>,
auto_mode: bool,
} }
impl EvolutionMiddleware { impl EvolutionMiddleware {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
pending: Arc::new(RwLock::new(Vec::new())), pending: Arc::new(RwLock::new(Vec::new())),
auto_mode: false,
} }
} }
/// Create with auto mode enabled
pub fn new_auto() -> Self {
Self {
pending: Arc::new(RwLock::new(Vec::new())),
auto_mode: true,
}
}
/// Check if auto mode is enabled
pub fn is_auto_mode(&self) -> bool {
self.auto_mode
}
/// 添加一个待确认的进化事件 /// 添加一个待确认的进化事件
pub async fn add_pending(&self, evolution: PendingEvolution) { pub async fn add_pending(&self, evolution: PendingEvolution) {
self.pending.write().await.push(evolution); self.pending.write().await.push(evolution);
@@ -73,7 +90,12 @@ impl AgentMiddleware for EvolutionMiddleware {
return Ok(MiddlewareDecision::Continue); return Ok(MiddlewareDecision::Continue);
} }
// 只移除第一个事件,保留后续事件留待下次注入 // Auto mode: don't inject into prompt, leave for kernel to process
if self.auto_mode {
return Ok(MiddlewareDecision::Continue);
}
// Suggest mode: 只移除第一个事件,保留后续事件留待下次注入
let to_inject = { let to_inject = {
let mut pending = self.pending.write().await; let mut pending = self.pending.write().await;
if pending.is_empty() { if pending.is_empty() {

View File

@@ -602,9 +602,11 @@ fn parse_uri(uri: &str) -> Result<(String, MemoryType, String), String> {
/// Configure embedding for semantic memory search /// Configure embedding for semantic memory search
/// Configures SqliteStorage (VikingStorage) embedding for FTS5 + semantic search. /// Configures SqliteStorage (VikingStorage) embedding for FTS5 + semantic search.
/// Also propagates to Kernel's skill router and memory retriever.
// @connected // @connected
#[tauri::command] #[tauri::command]
pub async fn viking_configure_embedding( pub async fn viking_configure_embedding(
kernel_state: tauri::State<'_, crate::kernel_commands::KernelState>,
provider: String, provider: String,
api_key: String, api_key: String,
model: Option<String>, model: Option<String>,
@@ -621,12 +623,23 @@ pub async fn viking_configure_embedding(
let client_viking = crate::llm::EmbeddingClient::new(config_viking); let client_viking = crate::llm::EmbeddingClient::new(config_viking);
let adapter = crate::embedding_adapter::TauriEmbeddingAdapter::new(client_viking); let adapter = crate::embedding_adapter::TauriEmbeddingAdapter::new(client_viking);
let arc_adapter = std::sync::Arc::new(adapter);
// 1. Configure SqliteStorage (existing behavior)
storage storage
.configure_embedding(std::sync::Arc::new(adapter)) .configure_embedding(arc_adapter.clone())
.await .await
.map_err(|e| format!("Failed to configure embedding: {}", e))?; .map_err(|e| format!("Failed to configure embedding: {}", e))?;
// 2. Propagate to Kernel for skill router + memory retriever
{
let mut kernel_lock = kernel_state.lock().await;
if let Some(ref mut k) = *kernel_lock {
k.set_embedding_client(arc_adapter);
tracing::info!("[VikingCommands] Embedding propagated to Kernel skill router + memory retriever");
}
}
tracing::info!("[VikingCommands] Embedding configured with provider: {}", provider); tracing::info!("[VikingCommands] Embedding configured with provider: {}", provider);
Ok(EmbeddingConfigResult { Ok(EmbeddingConfigResult {