110 lines
4.2 KiB
Python
110 lines
4.2 KiB
Python
"""skill_view repeat-view dedup registry.
|
|
|
|
Per-task cache of (skill name, file_path) -> (skill file mtime+size). On a
|
|
repeat view of an UNCHANGED skill file, ``tools.skills_tool`` returns a short
|
|
stub instead of re-sending the full content — the earlier tool result in this
|
|
conversation already carries it verbatim. Cleared on context compression via
|
|
``reset_skill_view_dedup()`` (wired next to read_file's reset_file_dedup)
|
|
because after compression the original content is summarized away.
|
|
|
|
Every name is re-imported into ``tools.skills_tool``; the tracker state lives
|
|
here and only here.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import threading
|
|
from typing import Dict
|
|
|
|
_skill_view_tracker: Dict[str, Dict[tuple, tuple]] = {}
|
|
_skill_view_tracker_lock = threading.Lock()
|
|
_SKILL_VIEW_DEDUP_CAP = 200
|
|
|
|
_SKILL_VIEW_DEDUP_MESSAGE = (
|
|
"Skill content unchanged since it was loaded earlier in this "
|
|
"conversation — refer to the earlier skill_view result; it is still "
|
|
"current and complete. (Re-issued after context compression, this "
|
|
"returns the full content again.)"
|
|
)
|
|
|
|
|
|
def _skill_view_fingerprint(payload: dict) -> tuple | None:
|
|
"""Stat the skill file a successful skill_view served, for change detection."""
|
|
src = payload.get("_source_path")
|
|
if not src:
|
|
return None
|
|
try:
|
|
st = os.stat(src)
|
|
return (src, st.st_mtime_ns, st.st_size)
|
|
except OSError:
|
|
return None
|
|
|
|
|
|
def _record_skill_view(task_id, name, file_path, payload: dict) -> None:
|
|
"""Record a served skill_view so an identical repeat can be deduped."""
|
|
if not task_id:
|
|
return
|
|
# Never dedup setup-needed views: readiness depends on config/env state
|
|
# that can change without the skill file changing, and the model must
|
|
# see the refreshed setup status on a re-view.
|
|
if payload.get("setup_needed") or payload.get("readiness_status") == "setup_needed":
|
|
return
|
|
fp = _skill_view_fingerprint(payload)
|
|
if fp is None:
|
|
return
|
|
key = (str(payload.get("name") or name), file_path or "")
|
|
with _skill_view_tracker_lock:
|
|
cache = _skill_view_tracker.setdefault(str(task_id), {})
|
|
cache[key] = fp
|
|
while len(cache) > _SKILL_VIEW_DEDUP_CAP:
|
|
try:
|
|
cache.pop(next(iter(cache)))
|
|
except (StopIteration, KeyError):
|
|
break
|
|
|
|
|
|
def _check_skill_view_dedup(task_id, name, file_path) -> str | None:
|
|
"""Return a dedup stub when this exact skill file was already served
|
|
to this task and is unchanged on disk; None otherwise."""
|
|
if not task_id:
|
|
return None
|
|
with _skill_view_tracker_lock:
|
|
cache = _skill_view_tracker.get(str(task_id))
|
|
if not cache:
|
|
return None
|
|
# The record key uses the RESOLVED name; check both the raw arg and
|
|
# resolved forms so 'category/skill' and bare-name views coalesce.
|
|
for key, (src, mtime_ns, size) in list(cache.items()):
|
|
rec_name, rec_fp = key
|
|
if rec_fp != (file_path or ""):
|
|
continue
|
|
n = str(name)
|
|
if rec_name != n and not n.endswith("/" + rec_name) \
|
|
and not rec_name.endswith("/" + n) and n.split(":")[-1] != rec_name:
|
|
continue
|
|
try:
|
|
st = os.stat(src)
|
|
if (st.st_mtime_ns, st.st_size) != (mtime_ns, size):
|
|
cache.pop(key, None)
|
|
return None
|
|
except OSError:
|
|
cache.pop(key, None)
|
|
return None
|
|
return json.dumps({
|
|
"success": True, "status": "unchanged", "name": rec_name,
|
|
"file": file_path or "SKILL.md", "dedup": True,
|
|
"content_returned": False, "message": _SKILL_VIEW_DEDUP_MESSAGE,
|
|
}, ensure_ascii=False)
|
|
return None
|
|
|
|
|
|
def reset_skill_view_dedup(task_id: str | None = None) -> None:
|
|
"""Clear the skill_view dedup cache (all tasks when task_id is None). Called on
|
|
context compression: the original content is summarized away, so a re-view
|
|
must return full content again."""
|
|
with _skill_view_tracker_lock:
|
|
if task_id is None:
|
|
_skill_view_tracker.clear()
|
|
else:
|
|
_skill_view_tracker.pop(str(task_id), None)
|