Cloud kimi-k2.5 executor for weak tiers + multi-strategy playbook retrieval
Two coupled changes from the 2026 agent-memory research + tool
asymmetry findings.
SCENARIO (weak-tier cloud substitute):
qwen2.5 collapsed to 0/14 across the basic/minimal tool_levels.
Replace with cloud kimi-k2.5 on Ollama Cloud — same family as k2.6
(pro-tier locked today, on J's upgrade path). Plumb cloud flag
through ACTIVE_EXECUTOR_CLOUD / ACTIVE_REVIEWER_CLOUD into
generateContinuable so executor/reviewer can route to cloud when
tool_level requires. think:false supported by Kimi family.
Tool level mapping (revised):
full — qwen3.5 local + qwen3 local + cloud gpt-oss:120b T3 + rescue
local — qwen3.5 local + qwen3 local + local gpt-oss:20b T3 + rescue
basic — kimi-k2.5 cloud + qwen3 local + local T3, no rescue
minimal — kimi-k2.5 cloud + qwen3 local, no T3, no rescue.
Playbook inheritance alone on the decision path.
This is the honest version of J's "minimal tools still works via
inheritance" hypothesis — with the executor no longer broken at the
tokenizer level, we can actually measure whether playbook retrieval
substitutes for missing overseers.
PLAYBOOK_MEMORY (multi-strategy retrieval):
Zep / Mem0 research shows multi-strategy rerank (semantic + keyword +
graph + temporal) outperforms single-strategy cosine. Lakehouse now
has a two-tier:
1. Exact (role, city, state) match: skip cosine, assign similarity=1.0,
take up to top_k/2+1 slots. These are identity-class neighbors —
the strongest possible signal.
2. Cosine fallback within the same (city, state) but different role:
fills remaining slots.
Exposed as compute_boost_for_filtered_with_role(target_geo, target_role).
Backwards-compatible: compute_boost_for_filtered forwards with role=None
so existing callers keep their current behavior.
Service.rs wires both: extract_target_geo and extract_target_role pull
from the executor's SQL filter. grab_eq_value is factored out of
extract_target_geo so both lookups share one parser. Diagnostic log
now prints target_role alongside target_geo for every hybrid_search:
playbook_boost: boosts=88 sources=39 parsed=39 matched=5
target_geo=Some(("Nashville", "TN")) target_role=Some("Welder")
Verified: Nashville Welder query returns 5/10 boosted workers in
top_k with clean role+geo provenance.
Research sources: atlan.com Agent Memory Frameworks 2026, Mem0 paper
(arxiv 2504.19413), Zep/Graphiti LongMemEval comparison, ossinsight
Agent Memory Race 2026.
kimi-k2.6 on current key returns 403 — pro-tier upgrade required.
kimi-k2.5 is the substitute today; swap to k2.6 by renaming one line
in applyToolLevel once the subscription lands.
This commit is contained in:
@@ -235,6 +235,26 @@ impl PlaybookMemory {
|
||||
top_k_playbooks: usize,
|
||||
base_weight: f32,
|
||||
target_geo: Option<(&str, &str)>,
|
||||
) -> HashMap<(String, String, String), BoostEntry> {
|
||||
self.compute_boost_for_filtered_with_role(query_embedding, top_k_playbooks, base_weight, target_geo, None).await
|
||||
}
|
||||
|
||||
/// Variant that also accepts a target role for pre-filtering.
|
||||
/// Multi-strategy retrieval: exact (role, city, state) matches skip
|
||||
/// cosine entirely and earn the maximum boost, since identity on
|
||||
/// those three fields is the strongest possible similarity signal.
|
||||
/// Remaining entries (within the same city+state but different
|
||||
/// role, or unknown role) go through the normal cosine path as a
|
||||
/// fallback. This addresses the 2026 agent-memory finding that
|
||||
/// multi-strategy parallel retrieval with rerank outperforms
|
||||
/// single-strategy semantic search.
|
||||
pub async fn compute_boost_for_filtered_with_role(
|
||||
&self,
|
||||
query_embedding: &[f32],
|
||||
top_k_playbooks: usize,
|
||||
base_weight: f32,
|
||||
target_geo: Option<(&str, &str)>,
|
||||
target_role: Option<&str>,
|
||||
) -> HashMap<(String, String, String), BoostEntry> {
|
||||
let state = self.state.read().await;
|
||||
let entries = state.entries.clone();
|
||||
@@ -246,11 +266,11 @@ impl PlaybookMemory {
|
||||
}
|
||||
drop(state);
|
||||
|
||||
// Brute-force cosine. Empty / missing embeddings just skip.
|
||||
// When target_geo is set, pre-filter to matching playbooks BEFORE
|
||||
// cosine sort — that way top-k is within the city, not across
|
||||
// all cities.
|
||||
let mut scored: Vec<(f32, &PlaybookEntry)> = entries
|
||||
// Pre-filter by target_geo (city, state) before cosine. When
|
||||
// target_geo is set, only playbooks from that city go into the
|
||||
// ranking pool — prevents globally-popular semantic neighbors
|
||||
// from drowning out the city's local successful playbooks.
|
||||
let geo_filtered: Vec<&PlaybookEntry> = entries
|
||||
.iter()
|
||||
.filter(|e| match (target_geo, &e.city, &e.state) {
|
||||
(None, _, _) => true,
|
||||
@@ -259,10 +279,45 @@ impl PlaybookMemory {
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
.filter_map(|e| e.embedding.as_ref().map(|v| (cosine(query_embedding, v), e)))
|
||||
.collect();
|
||||
scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
scored.truncate(top_k_playbooks.max(1));
|
||||
|
||||
// Multi-strategy: split the geo-filtered pool into (exact role
|
||||
// match) vs (other). Exact matches skip cosine — they're already
|
||||
// the strongest signal possible. Operations are shaped
|
||||
// "fill: Welder x3 in Toledo, OH" so we match role by checking
|
||||
// whether `fill: {role} ` appears in the operation string,
|
||||
// case-insensitive.
|
||||
let mut exact_matches: Vec<&PlaybookEntry> = Vec::new();
|
||||
let mut cosine_pool: Vec<(f32, &PlaybookEntry)> = Vec::new();
|
||||
let role_needle = target_role
|
||||
.map(|r| format!("fill: {} ", r).to_ascii_lowercase());
|
||||
for e in geo_filtered {
|
||||
let is_exact = role_needle.as_ref()
|
||||
.map(|needle| e.operation.to_ascii_lowercase().contains(needle))
|
||||
.unwrap_or(false);
|
||||
if is_exact {
|
||||
exact_matches.push(e);
|
||||
} else if let Some(v) = &e.embedding {
|
||||
cosine_pool.push((cosine(query_embedding, v), e));
|
||||
}
|
||||
}
|
||||
cosine_pool.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
// Allocate top_k across the two pools — exact matches get first
|
||||
// priority (up to min(exact_count, top_k/2) slots), then cosine
|
||||
// fills the rest. This is rerank with hard preference for
|
||||
// identity matches.
|
||||
let exact_take = exact_matches.len().min(top_k_playbooks.max(1) / 2 + 1);
|
||||
let cosine_take = top_k_playbooks.saturating_sub(exact_take);
|
||||
|
||||
// Score exact matches with max similarity (1.0) so downstream
|
||||
// weighting treats them as the strongest possible signal.
|
||||
let mut scored: Vec<(f32, &PlaybookEntry)> = exact_matches
|
||||
.into_iter()
|
||||
.take(exact_take)
|
||||
.map(|e| (1.0_f32, e))
|
||||
.collect();
|
||||
scored.extend(cosine_pool.into_iter().take(cosine_take));
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let mut boosts: HashMap<(String, String, String), BoostEntry> = HashMap::new();
|
||||
|
||||
Reference in New Issue
Block a user