fix(growth,skills,kernel): Phase 0 地基修复 — 经验积累覆盖 + Skill 工具调用
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
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。
This commit is contained in:
@@ -118,10 +118,40 @@ impl ExperienceStore {
|
||||
&self.viking
|
||||
}
|
||||
|
||||
/// Store (or overwrite) an experience. The URI is derived from
|
||||
/// `agent_id + pain_pattern`, ensuring one experience per pattern.
|
||||
/// Store an experience, merging with existing if the same pain pattern
|
||||
/// already exists for this agent. Reuse-count is preserved and incremented
|
||||
/// rather than reset to zero on re-extraction.
|
||||
pub async fn store_experience(&self, exp: &Experience) -> zclaw_types::Result<()> {
|
||||
let uri = exp.uri();
|
||||
|
||||
// If an experience with this URI already exists, merge instead of overwrite.
|
||||
if let Some(existing_entry) = self.viking.get(&uri).await? {
|
||||
if let Ok(existing) = serde_json::from_str::<Experience>(&existing_entry.content) {
|
||||
let merged = Experience {
|
||||
id: existing.id.clone(),
|
||||
reuse_count: existing.reuse_count + 1,
|
||||
created_at: existing.created_at,
|
||||
updated_at: Utc::now(),
|
||||
// New data takes precedence for content fields
|
||||
pain_pattern: exp.pain_pattern.clone(),
|
||||
agent_id: exp.agent_id.clone(),
|
||||
context: exp.context.clone(),
|
||||
solution_steps: exp.solution_steps.clone(),
|
||||
outcome: exp.outcome.clone(),
|
||||
industry_context: exp.industry_context.clone().or(existing.industry_context.clone()),
|
||||
source_trigger: exp.source_trigger.clone().or(existing.source_trigger.clone()),
|
||||
tool_used: exp.tool_used.clone().or(existing.tool_used.clone()),
|
||||
};
|
||||
return self.write_entry(&uri, &merged).await;
|
||||
}
|
||||
}
|
||||
|
||||
self.write_entry(&uri, exp).await
|
||||
}
|
||||
|
||||
/// Low-level write: serialises the experience into a MemoryEntry and
|
||||
/// persists it through the VikingAdapter.
|
||||
async fn write_entry(&self, uri: &str, exp: &Experience) -> zclaw_types::Result<()> {
|
||||
let content = serde_json::to_string(exp)?;
|
||||
let mut keywords = vec![exp.pain_pattern.clone()];
|
||||
keywords.extend(exp.solution_steps.iter().take(3).cloned());
|
||||
@@ -133,7 +163,7 @@ impl ExperienceStore {
|
||||
}
|
||||
|
||||
let entry = MemoryEntry {
|
||||
uri,
|
||||
uri: uri.to_string(),
|
||||
memory_type: MemoryType::Experience,
|
||||
content,
|
||||
keywords,
|
||||
@@ -197,7 +227,7 @@ impl ExperienceStore {
|
||||
let mut updated = exp.clone();
|
||||
updated.reuse_count += 1;
|
||||
updated.updated_at = Utc::now();
|
||||
if let Err(e) = self.store_experience(&updated).await {
|
||||
if let Err(e) = self.write_entry(&exp.uri(), &updated).await {
|
||||
warn!("[ExperienceStore] Failed to increment reuse for {}: {}", exp.id, e);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +319,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_overwrites_same_pattern() {
|
||||
async fn test_store_merges_same_pattern() {
|
||||
let viking = Arc::new(VikingAdapter::in_memory());
|
||||
let store = ExperienceStore::new(viking);
|
||||
|
||||
@@ -303,13 +333,19 @@ mod tests {
|
||||
"agent-1", "packaging", "v2 updated",
|
||||
vec!["new step".into()], "better",
|
||||
);
|
||||
// Force same URI by reusing the ID logic — same pattern → same URI.
|
||||
// Same pattern → same URI → should merge, not overwrite.
|
||||
store.store_experience(&exp_v2).await.unwrap();
|
||||
|
||||
let found = store.find_by_agent("agent-1").await.unwrap();
|
||||
// Should be overwritten, not duplicated (same URI).
|
||||
// Should be merged into one entry, not duplicated.
|
||||
assert_eq!(found.len(), 1);
|
||||
// Content fields updated to v2.
|
||||
assert_eq!(found[0].context, "v2 updated");
|
||||
assert_eq!(found[0].solution_steps[0], "new step");
|
||||
// Reuse count incremented (was 0, now 1).
|
||||
assert_eq!(found[0].reuse_count, 1);
|
||||
// Original ID and created_at preserved.
|
||||
assert_eq!(found[0].id, exp_v1.id);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -376,4 +412,26 @@ mod tests {
|
||||
assert_eq!(found_a.len(), 1);
|
||||
assert_eq!(found_a[0].pain_pattern, "packaging");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reuse_count_accumulates_across_repeated_patterns() {
|
||||
let viking = Arc::new(VikingAdapter::in_memory());
|
||||
let store = ExperienceStore::new(viking);
|
||||
|
||||
// Store the same pattern 4 times (simulating 4 conversations)
|
||||
for i in 0..4 {
|
||||
let exp = Experience::new(
|
||||
"agent-1", "logistics delay", &format!("context v{}", i),
|
||||
vec![format!("step {}", i)], &format!("outcome {}", i),
|
||||
);
|
||||
store.store_experience(&exp).await.unwrap();
|
||||
}
|
||||
|
||||
let found = store.find_by_agent("agent-1").await.unwrap();
|
||||
assert_eq!(found.len(), 1);
|
||||
// First store: reuse_count=0, then 1, 2, 3 after each re-store.
|
||||
assert_eq!(found[0].reuse_count, 3);
|
||||
// Content should reflect the latest version.
|
||||
assert_eq!(found[0].context, "context v3");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user