mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
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:
parent
bd584da2f4
commit
53ad7ba2dc
14 changed files with 559 additions and 81 deletions
|
|
@ -21,6 +21,7 @@ except Exception: # pragma: no cover
|
|||
MAX_ACTIVE_SKILLS = 20
|
||||
ACTIVE_SKILLS_PLUGIN_NAME = "_skills"
|
||||
AGENT_DATA_NAME_LOADED_SKILLS = "loaded_skills"
|
||||
CONTEXT_DATA_NAME_LOADED_SKILLS = AGENT_DATA_NAME_LOADED_SKILLS
|
||||
CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS = "skills_chat_active"
|
||||
CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS = "skills_chat_disabled"
|
||||
CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS = "skills_chat_visible"
|
||||
|
|
@ -451,6 +452,20 @@ def load_skill_for_agent(
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
def skill_instruction_name(message: Any) -> str:
|
||||
match message:
|
||||
case {
|
||||
"content": {
|
||||
"skill_instructions": {
|
||||
"content_included": included,
|
||||
"name": name,
|
||||
}
|
||||
}
|
||||
} if included:
|
||||
return str(name or "").strip()
|
||||
return ""
|
||||
|
||||
|
||||
def _get_skill_files(skill_dir: Path) -> str:
|
||||
"""Get file tree for skill directory."""
|
||||
if not skill_dir.exists():
|
||||
|
|
@ -830,19 +845,77 @@ def get_active_skills(agent: Agent | None) -> list[ActiveSkillEntry]:
|
|||
return _build_active_skills(agent, limit=get_max_active_skills(agent=agent))
|
||||
|
||||
|
||||
def get_loaded_skill_entries(agent: Agent | None) -> list[ActiveSkillEntry]:
|
||||
def _normalize_loaded_skill_names(raw: Any) -> list[str]:
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
|
||||
names: list[str] = []
|
||||
for value in raw:
|
||||
name = str(value or "").strip()
|
||||
if name and name not in names:
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def get_loaded_skill_names(agent: Agent | None) -> list[str]:
|
||||
if not agent:
|
||||
return []
|
||||
|
||||
loaded = getattr(agent, "data", {}).get(AGENT_DATA_NAME_LOADED_SKILLS)
|
||||
if not isinstance(loaded, list):
|
||||
return []
|
||||
context = getattr(agent, "context", None)
|
||||
if context and hasattr(context, "get_data"):
|
||||
names = _normalize_loaded_skill_names(
|
||||
context.get_data(CONTEXT_DATA_NAME_LOADED_SKILLS)
|
||||
)
|
||||
if names:
|
||||
data = getattr(agent, "data", None)
|
||||
if isinstance(data, dict):
|
||||
data.pop(AGENT_DATA_NAME_LOADED_SKILLS, None)
|
||||
return names
|
||||
|
||||
return [
|
||||
{"name": str(skill_name).strip()}
|
||||
for skill_name in loaded
|
||||
if str(skill_name).strip()
|
||||
]
|
||||
legacy_names = _normalize_loaded_skill_names(
|
||||
getattr(agent, "data", {}).get(AGENT_DATA_NAME_LOADED_SKILLS)
|
||||
)
|
||||
if legacy_names:
|
||||
set_loaded_skill_names(agent, legacy_names)
|
||||
return legacy_names
|
||||
|
||||
|
||||
def set_loaded_skill_names(agent: Agent | None, skill_names: Any) -> list[str]:
|
||||
names = _normalize_loaded_skill_names(skill_names)[-MAX_ACTIVE_SKILLS:]
|
||||
if not agent:
|
||||
return names
|
||||
|
||||
context = getattr(agent, "context", None)
|
||||
if context and hasattr(context, "set_data"):
|
||||
context.set_data(CONTEXT_DATA_NAME_LOADED_SKILLS, names or None)
|
||||
data = getattr(agent, "data", None)
|
||||
if isinstance(data, dict):
|
||||
data.pop(AGENT_DATA_NAME_LOADED_SKILLS, None)
|
||||
return names
|
||||
|
||||
data = getattr(agent, "data", None)
|
||||
if isinstance(data, dict):
|
||||
data[AGENT_DATA_NAME_LOADED_SKILLS] = names
|
||||
return names
|
||||
|
||||
|
||||
def add_loaded_skill_name(
|
||||
agent: Agent | None,
|
||||
skill_name: str,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
) -> list[str]:
|
||||
name = str(skill_name or "").strip()
|
||||
if not name:
|
||||
return get_loaded_skill_names(agent)
|
||||
|
||||
names = [loaded for loaded in get_loaded_skill_names(agent) if loaded != name]
|
||||
names.append(name)
|
||||
return set_loaded_skill_names(agent, names[-(limit or MAX_ACTIVE_SKILLS):])
|
||||
|
||||
|
||||
def get_loaded_skill_entries(agent: Agent | None) -> list[ActiveSkillEntry]:
|
||||
return [{"name": skill_name} for skill_name in get_loaded_skill_names(agent)]
|
||||
|
||||
|
||||
def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
|
||||
|
|
@ -850,17 +923,9 @@ def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
|
|||
if not agent or not normalized:
|
||||
return False
|
||||
|
||||
data = getattr(agent, "data", None)
|
||||
if not isinstance(data, dict):
|
||||
return False
|
||||
|
||||
loaded = data.get(AGENT_DATA_NAME_LOADED_SKILLS)
|
||||
if not isinstance(loaded, list):
|
||||
return False
|
||||
|
||||
next_loaded: list[str] = []
|
||||
removed = False
|
||||
for skill_name in loaded:
|
||||
for skill_name in get_loaded_skill_names(agent):
|
||||
loaded_entry = _normalize_active_skill_entry(str(skill_name))
|
||||
if loaded_entry and _entries_match(loaded_entry, normalized):
|
||||
removed = True
|
||||
|
|
@ -868,7 +933,7 @@ def unload_agent_skill(agent: Agent | None, entry: Any) -> bool:
|
|||
next_loaded.append(skill_name)
|
||||
|
||||
if removed:
|
||||
data[AGENT_DATA_NAME_LOADED_SKILLS] = next_loaded
|
||||
set_loaded_skill_names(agent, next_loaded)
|
||||
return removed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
- `delete_skill(skill_path: str) -> None`: Delete a skill directory.
|
||||
- `find_skill(skill_name: str, agent: Agent | None=..., include_content: bool=..., include_hidden: bool=...) -> Optional[Skill]`
|
||||
- `load_skill_for_agent(skill_name: str, agent: Agent | None=...) -> str`: Load skill and format it as a complete string for agent context.
|
||||
- `skill_instruction_name(message: Any) -> str`
|
||||
- `_get_skill_files(skill_dir: Path) -> str`: Get file tree for skill directory.
|
||||
- `search_skills(query: str, limit: int=..., agent: Agent | None=..., include_hidden: bool=...) -> List[Skill]`
|
||||
- `validate_skill(skill: Skill) -> List[str]`
|
||||
|
|
@ -45,13 +46,17 @@
|
|||
- `get_scope_hidden_skills(agent: Agent | None) -> list[ActiveSkillEntry]`
|
||||
- `get_chat_active_skills(context: Any | None) -> list[ActiveSkillEntry]`
|
||||
- `get_chat_disabled_skills(context: Any | None) -> list[ActiveSkillEntry]`
|
||||
- Notable constants/configuration names: `MAX_ACTIVE_SKILLS`, `ACTIVE_SKILLS_PLUGIN_NAME`, `AGENT_DATA_NAME_LOADED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS`, `CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS`, `_NAME_RE`.
|
||||
- `get_loaded_skill_names(agent: Agent | None) -> list[str]`
|
||||
- `set_loaded_skill_names(agent: Agent | None, skill_names: Any) -> list[str]`
|
||||
- `add_loaded_skill_name(agent: Agent | None, skill_name: str, limit: int | None=...) -> list[str]`
|
||||
- Notable constants/configuration names: `MAX_ACTIVE_SKILLS`, `ACTIVE_SKILLS_PLUGIN_NAME`, `AGENT_DATA_NAME_LOADED_SKILLS`, `CONTEXT_DATA_NAME_LOADED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_ACTIVE_SKILLS`, `CONTEXT_DATA_NAME_CHAT_DISABLED_SKILLS`, `CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS`, `_NAME_RE`.
|
||||
|
||||
## Runtime Contracts
|
||||
|
||||
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
|
||||
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
|
||||
- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, secret handling.
|
||||
- Loaded skill names are chat-wide context data under `CONTEXT_DATA_NAME_LOADED_SKILLS`; legacy agent-local `loaded_skills` lists are migrated into context data and cleared when read.
|
||||
- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, context data, secret handling.
|
||||
- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
|
||||
|
||||
## Key Concepts
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue