Skip to content

Commit 2d4e0e3

Browse files
committed
feat(tiers): expand skill and tool management in tier evaluations
- Updated tier evaluation functions to include skills alongside tools, enhancing the logic for determining unused and promoted entities. - Refactored `_tool_unused_at_t2` and `crystallize_successful_tool_promotions_at_epoch` to accommodate skills, ensuring consistent behavior across both entity types. - Introduced new functions for handling shadow evaluations of skills, improving integration with the existing tier management system. - Enhanced tests to cover the new skill functionalities and ensure correct tier transitions for both tools and skills. This update improves the overall functionality of the tier management system by providing better support for skills, aligning with recent enhancements in skills management.
1 parent 79e31e8 commit 2d4e0e3

31 files changed

Lines changed: 3632 additions & 71 deletions

‎src/cyt/tiers/adapters/skills.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,35 @@ def resolve_tiered_skill_matches(
811811
partition = manager.partition_skills(resolved_entries, config)
812812
manager.record_skill_candidates(partition.search_entries, config)
813813

814+
if query.strip():
815+
dormant_ids = [
816+
entity_id
817+
for entry in resolved_entries
818+
if (entity_id := skill_entity_id(entry))
819+
and partition.tier_by_skill.get(entity_id) == Tier.DORMANT
820+
]
821+
if dormant_ids:
822+
skills_by_id: dict[str, dict[str, Any]] = {}
823+
for entry in resolved_entries:
824+
entity_id = skill_entity_id(entry)
825+
if not entity_id:
826+
continue
827+
document = entry.document if isinstance(entry.document, dict) else {}
828+
skills_by_id[entity_id] = {
829+
"name": document.get("name") or entry.doc_id,
830+
"description": document.get("description") or "",
831+
"doc_id": entry.doc_id,
832+
}
833+
from cyt.tiers.shadow import schedule_skill_shadow_evaluation
834+
835+
schedule_skill_shadow_evaluation(
836+
config=config,
837+
query=query,
838+
dormant_entity_ids=dormant_ids,
839+
skills_by_id=skills_by_id,
840+
manager=manager,
841+
)
842+
814843
search_pool = prepare_skill_entries_for_tier_search(
815844
partition.search_entries,
816845
partition.tier_by_skill,

‎src/cyt/tiers/evaluator.py‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,11 @@ def _min_exposure_met(state: EntityTierState, minimum: int) -> bool:
8181

8282

8383
def _tool_unused_at_t2(state: EntityTierState) -> bool:
84-
"""True when a tool is still default T2 but never injected or invoked.
84+
"""True when a tool or skill is still default T2 but never injected or invoked.
8585
8686
BM25 candidacy alone does not preserve T2 across epochs; only injection/use signals do.
8787
"""
88-
if state.kind != "tool":
88+
if state.kind not in ("tool", "skill"):
8989
return False
9090
stats = state.stats
9191
return (
@@ -278,10 +278,10 @@ def crystallize_successful_tool_promotions_at_epoch(
278278
*,
279279
epoch: EpochState,
280280
) -> list[TierTransition]:
281-
"""Promote tools with successful use this epoch to stable HOT (recent signal wins)."""
281+
"""Promote tools/skills with successful use this epoch to stable HOT (recent signal wins)."""
282282
transitions: list[TierTransition] = []
283283
for state in states.values():
284-
if state.kind != "tool" or not _epoch_had_success(state):
284+
if state.kind not in ("tool", "skill") or not _epoch_had_success(state):
285285
continue
286286
state.temp_promotion_until_ms = None
287287
state.overlap_tier = None
@@ -317,7 +317,11 @@ def expire_temporary_promotions(
317317
continue
318318
if state.effective_tier != state.stable_tier:
319319
old = state.effective_tier
320-
crystallize = state.kind == "tool" and old > state.stable_tier and state.stats.used > 0
320+
crystallize = (
321+
state.kind in ("tool", "skill")
322+
and old > state.stable_tier
323+
and state.stats.used > 0
324+
)
321325
if crystallize:
322326
old_stable = state.stable_tier
323327
state.stable_tier = old

‎src/cyt/tiers/manager.py‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,20 +795,36 @@ def record_skill_used(self, entity_id: str, *, config: dict[str, Any]) -> None:
795795
)
796796
if not canonical_id:
797797
return
798+
cfg = tier_section_config(config, kind="skill")
798799
epoch_ran = False
800+
sync_flush = False
799801
with self._state_lock:
800802
state = self._ensure_state(EntityKind.SKILL, canonical_id)
801803
if state is None:
802804
return
803805
state.stats.used += 1.0
806+
state.stats.epoch_used += 1.0
804807
self._mark_cycle_used(EntityKind.SKILL, canonical_id)
805808
if canonical_id not in self._last_injected_skills:
806809
state.stats.used_without_injection += 1.0
810+
fast_promote_on_tool_use(state, cfg=cfg)
811+
state.stats.last_seen_ms = int(time.time() * 1000)
807812
epoch_ran = self._touch_request(config)
813+
self._touch_stats_request(state)
814+
sync_flush = (
815+
state.temp_promotion_until_ms is not None
816+
or state.effective_tier > state.stable_tier
817+
)
808818
if not epoch_ran:
809-
self._finish_record(config)
819+
self._finish_record(config, sync_flush=sync_flush)
810820

811-
def apply_shadow_tool_hits(self, hits: list[tuple[str, float]], config: dict[str, Any]) -> None:
821+
def apply_shadow_tool_hits(
822+
self,
823+
hits: list[tuple[str, float]],
824+
config: dict[str, Any],
825+
*,
826+
tools_by_id: dict[str, dict[str, Any]] | None = None,
827+
) -> None:
812828
from cyt.tiers.adapters.tools import tool_entity_tracked_for_config
813829
from cyt.tiers.shadow import record_shadow_hits
814830

@@ -830,11 +846,34 @@ def apply_shadow_tool_hits(self, hits: list[tuple[str, float]], config: dict[str
830846
hits=tracked_hits,
831847
cfg=cfg,
832848
wake_cycle_id=self._epoch.wake_cycle_id,
849+
tools_by_id=tools_by_id,
833850
)
834851
if transitions and cfg.mode == TierMode.SHADOW:
835852
logger.debug("tool shadow transitions: %d", len(transitions))
836853
self._finish_record(config)
837854

855+
def apply_shadow_skill_hits(self, hits: list[tuple[str, float]], config: dict[str, Any]) -> None:
856+
from cyt.tiers.shadow import record_shadow_hits
857+
858+
if not tiers_active(config, kind="skill"):
859+
return
860+
cfg = tier_section_config(config, kind="skill")
861+
if not hits:
862+
return
863+
with self._state_lock:
864+
for entity_id, _score in hits:
865+
self._mark_cycle_shadow(EntityKind.SKILL, entity_id)
866+
transitions = record_shadow_hits(
867+
self._states,
868+
kind=EntityKind.SKILL,
869+
hits=hits,
870+
cfg=cfg,
871+
wake_cycle_id=self._epoch.wake_cycle_id,
872+
)
873+
if transitions and cfg.mode == TierMode.SHADOW:
874+
logger.debug("skill shadow transitions: %d", len(transitions))
875+
self._finish_record(config)
876+
838877
def status(
839878
self,
840879
config: dict[str, Any] | None = None,

‎src/cyt/tiers/shadow.py‎

Lines changed: 134 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def _run() -> None:
7070

7171
section = tier_section_config(config, kind="tool")
7272
hits = _lexical_shadow_hits(query, dormant_ids, tools_by_id)
73-
manager.apply_shadow_tool_hits(hits, config)
73+
manager.apply_shadow_tool_hits(hits, config, tools_by_id=tools_by_id)
7474
if section.mode == TierMode.SHADOW and hits:
7575
logger.debug("tool shadow hits: %d dormant tools", len(hits))
7676
except Exception:
@@ -107,19 +107,98 @@ def _run() -> None:
107107
_executor.submit(_run)
108108

109109

110+
def _entity_mcp_server(entity_id: str, tools_by_id: dict[str, dict[str, Any]]) -> str | None:
111+
tool = tools_by_id.get(entity_id)
112+
if tool is not None:
113+
from cyt.tool_examples.identity import resolve_mcp_server_and_tool
114+
115+
server, _bare = resolve_mcp_server_and_tool(tool)
116+
return server or None
117+
if ":" not in entity_id:
118+
return None
119+
wire_name = entity_id.split(":", 1)[1]
120+
from cyt.tool_examples.identity import resolve_mcp_server_and_tool_from_wire_name
121+
122+
server, _bare = resolve_mcp_server_and_tool_from_wire_name(wire_name)
123+
return server or None
124+
125+
126+
def _lexical_skill_shadow_hits(
127+
query: str,
128+
dormant_ids: list[str],
129+
skills_by_id: dict[str, dict[str, Any]],
130+
) -> list[tuple[str, float]]:
131+
q = query.lower()
132+
hits: list[tuple[str, float]] = []
133+
for entity_id in dormant_ids:
134+
skill = skills_by_id.get(entity_id)
135+
if skill is None:
136+
continue
137+
name = str(skill.get("name") or skill.get("doc_id") or "").lower()
138+
desc = str(skill.get("description") or "").lower()
139+
score = 0.0
140+
if name and name in q:
141+
score = 0.85
142+
elif name:
143+
parts = [part for part in name.replace("-", "_").split("_") if len(part) > 2]
144+
if any(part in q for part in parts):
145+
score = 0.55
146+
if desc:
147+
words = [word for word in desc.split() if len(word) > 4][:12]
148+
if any(word.lower() in q for word in words):
149+
score = max(score, 0.35)
150+
if score > 0.0:
151+
hits.append((entity_id, score))
152+
return hits
153+
154+
155+
def schedule_skill_shadow_evaluation(
156+
*,
157+
config: dict[str, Any],
158+
query: str,
159+
dormant_entity_ids: list[str],
160+
skills_by_id: dict[str, dict[str, Any]],
161+
manager: TierManager | NoOpTierManager,
162+
) -> None:
163+
if not tiers_active(config, kind="skill") or not query.strip():
164+
return
165+
if not dormant_entity_ids:
166+
return
167+
168+
def _run() -> None:
169+
try:
170+
from cyt.tiers.config import tier_section_config
171+
172+
section = tier_section_config(config, kind="skill")
173+
hits = _lexical_skill_shadow_hits(query, dormant_entity_ids, skills_by_id)
174+
manager.apply_shadow_skill_hits(hits, config)
175+
if section.mode == TierMode.SHADOW and hits:
176+
logger.debug("skill shadow hits: %d dormant skills", len(hits))
177+
except Exception:
178+
logger.exception("skill shadow evaluation failed")
179+
180+
with _executor_lock:
181+
_executor.submit(_run)
182+
183+
110184
def record_shadow_hits(
111185
states: dict[tuple[str, str], EntityTierState],
112186
*,
113187
kind: str,
114188
hits: list[tuple[str, float]],
115189
cfg: TierSectionConfig,
116190
wake_cycle_id: int,
191+
tools_by_id: dict[str, dict[str, Any]] | None = None,
117192
) -> list[Any]:
118193
from cyt.tiers.models import TierTransition
119194

120195
transitions: list[TierTransition] = []
196+
woken: set[str] = set()
121197
hit_set = {entity_id for entity_id, score in hits if score > 0}
122-
for entity_id in hit_set:
198+
199+
def _wake_entity(entity_id: str, *, query_relevance: float, sibling_activity: float) -> None:
200+
if entity_id in woken:
201+
return
123202
key = (kind, entity_id)
124203
state = states.get(key)
125204
if state is None:
@@ -130,21 +209,64 @@ def record_shadow_hits(
130209
effective_tier=Tier.DORMANT,
131210
)
132211
states[key] = state
133-
state.stats.shadow_evaluations += 1.0
134-
state.stats.shadow_hits += 1.0
135-
top_score = max((score for eid, score in hits if eid == entity_id), default=0.0)
212+
if entity_id in hit_set:
213+
state.stats.shadow_hits += 1.0
136214
wake = evaluate_fast_wake(
137215
state,
138216
cfg=cfg,
139217
wake_cycle_id=wake_cycle_id,
140-
query_relevance=top_score,
218+
query_relevance=query_relevance,
219+
sibling_activity=sibling_activity,
141220
)
142221
if wake is not None:
143222
transitions.append(wake)
144-
for state_key in states:
145-
if state_key[0] != kind:
146-
continue
147-
eid = state_key[1]
148-
if eid not in hit_set and states[state_key].effective_tier == Tier.DORMANT:
149-
states[state_key].stats.shadow_evaluations += 1.0
223+
woken.add(entity_id)
224+
225+
for entity_id in hit_set:
226+
key = (kind, entity_id)
227+
state = states.get(key)
228+
if state is None:
229+
state = EntityTierState(
230+
entity_id=entity_id,
231+
kind=kind,
232+
stable_tier=Tier.DORMANT,
233+
effective_tier=Tier.DORMANT,
234+
)
235+
states[key] = state
236+
state.stats.shadow_evaluations += 1.0
237+
top_score = max((score for eid, score in hits if eid == entity_id), default=0.0)
238+
_wake_entity(entity_id, query_relevance=top_score, sibling_activity=0.0)
239+
240+
server_max_score: dict[str, float] = {}
241+
if tools_by_id is not None:
242+
for entity_id in hit_set:
243+
server = _entity_mcp_server(entity_id, tools_by_id)
244+
if not server:
245+
continue
246+
top_score = max((score for eid, score in hits if eid == entity_id), default=0.0)
247+
server_max_score[server] = max(server_max_score.get(server, 0.0), top_score)
248+
249+
if tools_by_id is not None and server_max_score:
250+
for state_key, state in states.items():
251+
if state_key[0] != kind or state.effective_tier != Tier.DORMANT:
252+
continue
253+
entity_id = state_key[1]
254+
server = _entity_mcp_server(entity_id, tools_by_id)
255+
if not server or server not in server_max_score:
256+
continue
257+
if entity_id not in hit_set:
258+
state.stats.shadow_evaluations += 1.0
259+
_wake_entity(
260+
entity_id,
261+
query_relevance=server_max_score[server],
262+
sibling_activity=0.0 if entity_id in hit_set else 1.0,
263+
)
264+
else:
265+
for state_key in states:
266+
if state_key[0] != kind:
267+
continue
268+
eid = state_key[1]
269+
if eid not in hit_set and states[state_key].effective_tier == Tier.DORMANT:
270+
states[state_key].stats.shadow_evaluations += 1.0
271+
150272
return transitions

‎src/cyt/tiers/wake.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ def evaluate_fast_wake(
5959
query_relevance=query_relevance,
6060
sibling_activity=sibling_activity,
6161
)
62-
if pressure < cfg.wake_threshold and state.stats.used_without_injection <= 0:
62+
if (
63+
pressure < cfg.wake_threshold
64+
and state.stats.used_without_injection <= 0
65+
and query_relevance < cfg.wake_threshold
66+
):
6367
return None
6468
old = state.effective_tier
6569
state.effective_tier = Tier.COLD
@@ -116,8 +120,8 @@ def fast_promote_on_tool_use(
116120
*,
117121
cfg: TierSectionConfig | None = None,
118122
) -> TierTransition | None:
119-
"""Tools-only fast signal when the agent invoked the tool (any args)."""
120-
if state.kind != "tool":
123+
"""Fast signal when the agent invoked a tool or skill."""
124+
if state.kind not in ("tool", "skill"):
121125
return None
122126
now_ms = int(time.time() * 1000)
123127
if state.stable_tier >= Tier.HOT:
@@ -144,7 +148,7 @@ def fast_promote_on_tool_use(
144148
entity_id=state.entity_id,
145149
from_tier=old,
146150
to_tier=target,
147-
reason="tool_used",
151+
reason="skill_used" if state.kind == "skill" else "tool_used",
148152
temporary=True,
149153
)
150154

0 commit comments

Comments
 (0)