Persist loaded skills through compaction

Store explicitly loaded skill IDs as chat-wide context data while keeping full skill bodies in normal tool-result history. Reattach any loaded skill body that is no longer visible after compaction by reimporting the current skill from its source, without revision hashes or protocol reinjection.

Update compaction and summary prompts to preserve loaded skill names only, refresh DOX contracts, and add focused coverage for context-data persistence, legacy agent-data migration, duplicate suppression, and post-compaction reattachment.
This commit is contained in:
Alessandro 2026-06-23 16:32:21 +02:00
parent bd584da2f4
commit 53ad7ba2dc
14 changed files with 559 additions and 81 deletions

View file

@ -8,7 +8,7 @@ from helpers import skills as skills_helper
from helpers.print_style import PrintStyle
DATA_NAME_LOADED_SKILLS = skills_helper.AGENT_DATA_NAME_LOADED_SKILLS
DATA_NAME_LOADED_SKILLS = skills_helper.CONTEXT_DATA_NAME_LOADED_SKILLS
class SkillsTool(Tool):
@ -111,7 +111,7 @@ class SkillsTool(Tool):
skill_name = self._normalize_skill_name(
str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
)
return Response(message=self._load(skill_name), break_loop=False)
return self._load(skill_name)
if action == "read_file":
skill_name = self._normalize_skill_name(
str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
@ -185,11 +185,14 @@ class SkillsTool(Tool):
)
return "\n".join(lines)
def _load(self, skill_name: str) -> str:
def _load(self, skill_name: str) -> Response:
skill_name = self._normalize_skill_name(skill_name)
if not skill_name:
return "Error: 'skill_name' is required for action=load."
return Response(
message="Error: 'skill_name' is required for action=load.",
break_loop=False,
)
# Verify skill exists
skill = skills_helper.find_skill(
@ -198,18 +201,63 @@ class SkillsTool(Tool):
agent=self.agent,
)
if not skill:
return f"Error: skill not found: {skill_name!r}. Try skills_tool action=list or action=search."
return Response(
message=(
f"Error: skill not found: {skill_name!r}. "
"Try skills_tool action=list or action=search."
),
break_loop=False,
)
# Store skill name for fresh loading each turn
if not self.agent.data.get(DATA_NAME_LOADED_SKILLS):
self.agent.data[DATA_NAME_LOADED_SKILLS] = []
loaded = self.agent.data[DATA_NAME_LOADED_SKILLS]
if skill.name in loaded:
loaded.remove(skill.name)
loaded.append(skill.name)
self.agent.data[DATA_NAME_LOADED_SKILLS] = loaded[-max_loaded_skills():]
skill_data = skills_helper.load_skill_for_agent(
skill_name=skill.name,
agent=self.agent,
)
metadata = {
"name": skill.name,
"path": str(skill.path),
"source": "skills_tool:load",
"content_included": True,
}
return f"Loaded skill '{skill.name}' into Protocol."
skills_helper.add_loaded_skill_name(
self.agent,
skill.name,
limit=max_loaded_skills(),
)
if self._visible_skill_loaded(skill.name):
return Response(
message=(
f"Skill '{skill.name}' is already loaded in visible "
"chat history."
),
break_loop=False,
additional={
"skill_instructions": {
**metadata,
"content_included": False,
"already_loaded": True,
}
},
)
return Response(
message=skill_data,
break_loop=False,
additional={"skill_instructions": metadata},
)
def _visible_skill_loaded(self, skill_name: str) -> bool:
history_obj = getattr(self.agent, "history", None)
output = getattr(history_obj, "output", None)
if not callable(output):
return False
return any(
skills_helper.skill_instruction_name(message) == skill_name
for message in output()
)
def _read_file(self, skill_name: str, file_path: str) -> str:
if not skill_name:

View file

@ -15,6 +15,7 @@
- `get_log_object(self)`
- `async before_execution(self, **kwargs)`
- `async execute(self, **kwargs) -> Response`
- `_visible_skill_loaded(self, skill_name: str) -> bool`
- Top-level functions:
- `max_loaded_skills() -> int`
- Notable constants/configuration names: `DATA_NAME_LOADED_SKILLS`.
@ -25,13 +26,15 @@
- Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
- `SkillsTool` is a `Tool`.
- `SkillsTool` defines `execute(...)`.
- Loading a skill stores it for prompt Protocol injection on subsequent turns.
- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence.
- Loading a skill appends the full skill body as a normal tool-result history message with `skill_instructions` metadata containing name, path, source, and content visibility.
- Loaded skill IDs are stored in chat-wide context data.
- Duplicate loads omit the full body when the same skill name remains visible in model history.
- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence, chat history persistence.
- Imported dependency areas include: `__future__`, `helpers`, `helpers.print_style`, `helpers.tool`, `pathlib`, `typing`.
## Key Concepts
- Important called helpers/classes observed in the source: `str.strip.lower.replace`, `skill_name.strip`, `super.get_log_object`, `self._normalize_skill_name`, `self.get_log_object`, `skills_helper.list_skills`, `join`, `skills_helper.search_skills`, `skills_helper.find_skill`, `skill.path.resolve`, `Path`, `resolved.read_text`, `skill_name.startswith`, `skill_name.endswith`, `self._current_action`, `self.agent.context.log.log`, `Response`, `strip`, `loaded.remove`, `target.is_absolute`.
- Important called helpers/classes observed in the source: `str.strip.lower.replace`, `skill_name.strip`, `super.get_log_object`, `self._normalize_skill_name`, `self.get_log_object`, `skills_helper.list_skills`, `join`, `skills_helper.search_skills`, `skills_helper.find_skill`, `skills_helper.load_skill_for_agent`, `skills_helper.add_loaded_skill_name`, `skills_helper.skill_instruction_name`, `skill.path.resolve`, `Path`, `resolved.read_text`, `skill_name.startswith`, `skill_name.endswith`, `self._current_action`, `self.agent.context.log.log`, `Response`, `strip`, `target.is_absolute`.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance