mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Revert loaded skill history persistence
Revertsdb01d7c1c8and3c83b2eca2. Restores the prior loaded-skills prompt-extras behavior and removes the compaction reattachment metadata path.
This commit is contained in:
parent
8fda0ee69c
commit
bf2741990a
13 changed files with 32 additions and 389 deletions
|
|
@ -2,19 +2,17 @@
|
|||
|
||||
## Purpose
|
||||
|
||||
- Own prompt extras and history-output adjustments appended after primary message-loop prompt construction.
|
||||
- Own prompt extras appended after primary message-loop prompt construction.
|
||||
|
||||
## Ownership
|
||||
|
||||
- Ordered Python files own current datetime, skill recall/load context, loaded-skill reattachment, agent info, parallel job status, and workdir extras injection.
|
||||
- Ordered Python files own current datetime, skill recall/load context, agent info, parallel job status, and workdir extras injection.
|
||||
|
||||
## Local Contracts
|
||||
|
||||
- Keep injected content bounded and clearly attributed.
|
||||
- Preserve ordering where later prompt extras depend on earlier recall or load results.
|
||||
- Do not expose secrets or private files from workdir extras.
|
||||
- Explicitly loaded skill instructions belong in normal tool-result history; this hook may recall candidate skills, but must not reinject loaded skill bodies through prompt extras every turn.
|
||||
- If compression hides an explicitly loaded skill body, this hook may reattach the missing visible revision as a bounded normal tool-result history message.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,95 +1,38 @@
|
|||
from helpers.extension import Extension
|
||||
from helpers import skills, tokens
|
||||
from helpers import skills
|
||||
from tools.skills_tool import DATA_NAME_LOADED_SKILLS
|
||||
from agent import LoopData
|
||||
|
||||
|
||||
SKILL_REATTACHMENT_TOKEN_BUDGET = 12_000
|
||||
SKILL_REATTACHMENT_HEADER = (
|
||||
"Reattached loaded skill instructions after history compaction."
|
||||
)
|
||||
|
||||
|
||||
class IncludeLoadedSkills(Extension):
|
||||
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
|
||||
if not self.agent:
|
||||
return
|
||||
|
||||
loop_data.extras_persistent.pop("loaded_skills", None)
|
||||
extras = loop_data.extras_persistent
|
||||
|
||||
# Get loaded skills names
|
||||
skill_names = self.agent.data.get(DATA_NAME_LOADED_SKILLS)
|
||||
if not skill_names:
|
||||
return
|
||||
|
||||
# `skills_tool load` now appends full skill instructions as a normal
|
||||
# tool-result history message. Keep this legacy ledger pruned, but do
|
||||
# not reinject loaded skills through prompt extras every turn.
|
||||
# load skill text here
|
||||
content = ""
|
||||
visible_skill_names = []
|
||||
loaded_skills = []
|
||||
for skill_name in skill_names:
|
||||
skill = skills.find_skill(skill_name, agent=self.agent)
|
||||
if not skill:
|
||||
if not skills.find_skill(skill_name, agent=self.agent):
|
||||
continue
|
||||
visible_skill_names.append(skill.name)
|
||||
loaded_skills.append(skill)
|
||||
visible_skill_names.append(skill_name)
|
||||
skill_data = skills.load_skill_for_agent(skill_name=skill_name, agent=self.agent)
|
||||
content += "\n\n" + skill_data
|
||||
self.agent.data[DATA_NAME_LOADED_SKILLS] = visible_skill_names
|
||||
|
||||
self._reattach_missing_skill_bodies(loop_data, loaded_skills)
|
||||
|
||||
def _reattach_missing_skill_bodies(self, loop_data: LoopData, loaded_skills):
|
||||
if not self.agent or not loaded_skills:
|
||||
content = content.strip()
|
||||
if not content:
|
||||
return
|
||||
|
||||
visible_revisions = _visible_skill_revisions(loop_data.history_output)
|
||||
selected = []
|
||||
used_tokens = 0
|
||||
|
||||
for skill in reversed(loaded_skills):
|
||||
skill_data = skills.load_skill_for_agent(
|
||||
skill_name=skill.name,
|
||||
agent=self.agent,
|
||||
)
|
||||
revision = skills.skill_revision(skill_data)
|
||||
if (skill.name, revision) in visible_revisions:
|
||||
continue
|
||||
|
||||
message = f"{SKILL_REATTACHMENT_HEADER}\n\n{skill_data}"
|
||||
message_tokens = tokens.approximate_tokens(message)
|
||||
if used_tokens + message_tokens > SKILL_REATTACHMENT_TOKEN_BUDGET:
|
||||
continue
|
||||
|
||||
selected.append((skill, revision, message))
|
||||
used_tokens += message_tokens
|
||||
|
||||
for skill, revision, message in reversed(selected):
|
||||
history_message = self.agent.hist_add_tool_result(
|
||||
"skills_tool",
|
||||
message,
|
||||
skill_instructions={
|
||||
"name": skill.name,
|
||||
"path": str(skill.path),
|
||||
"revision": revision,
|
||||
"source": "skills_tool:reattach",
|
||||
"content_included": True,
|
||||
},
|
||||
)
|
||||
loop_data.history_output.extend(history_message.output())
|
||||
|
||||
|
||||
def _visible_skill_revisions(history_output) -> set[tuple[str, str]]:
|
||||
visible = set()
|
||||
for message in history_output or []:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
content = message.get("content")
|
||||
if not isinstance(content, dict):
|
||||
continue
|
||||
meta = content.get("skill_instructions")
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
if not meta.get("content_included"):
|
||||
continue
|
||||
name = str(meta.get("name") or "").strip()
|
||||
revision = str(meta.get("revision") or "").strip()
|
||||
if name and revision:
|
||||
visible.add((name, revision))
|
||||
return visible
|
||||
# Inject into extras
|
||||
extras["loaded_skills"] = self.agent.read_prompt(
|
||||
"agent.system.skills.loaded.md",
|
||||
skills=content,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
|
@ -452,10 +451,6 @@ def load_skill_for_agent(
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
def skill_revision(skill_data: str) -> str:
|
||||
return hashlib.sha256(skill_data.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
|
||||
def _get_skill_files(skill_dir: Path) -> str:
|
||||
"""Get file tree for skill directory."""
|
||||
if not skill_dir.exists():
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@
|
|||
- `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_revision(skill_data: str) -> 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]`
|
||||
|
|
@ -53,11 +52,11 @@
|
|||
- 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.
|
||||
- Imported dependency areas include: `__future__`, `dataclasses`, `hashlib`, `helpers`, `os`, `pathlib`, `re`, `typing`.
|
||||
- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- Important called helpers/classes observed in the source: `dataclass`, `re.compile`, `field`, `Path`, `root.rglob`, `results.sort`, `re.sub`, `path.read_text`, `text.splitlines`, `join.strip`, `parse_frontmatter`, `frontmatter_text.splitlines`, `_parse_frontmatter_fallback`, `split_frontmatter`, `str.strip`, `_coerce_list`, `Skill`, `get_skill_roots`, `_filter_hidden_skills`, `files.get_abs_path`, `hashlib.sha256`.
|
||||
- Important called helpers/classes observed in the source: `dataclass`, `re.compile`, `field`, `Path`, `root.rglob`, `results.sort`, `re.sub`, `path.read_text`, `text.splitlines`, `join.strip`, `parse_frontmatter`, `frontmatter_text.splitlines`, `_parse_frontmatter_fallback`, `split_frontmatter`, `str.strip`, `_coerce_list`, `Skill`, `get_skill_roots`, `_filter_hidden_skills`, `files.get_abs_path`.
|
||||
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
|
||||
|
||||
## Work Guidance
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
- Preserve chat history integrity and persistence after compaction.
|
||||
- Keep generated summaries bounded by configured model and token limits.
|
||||
- Do not discard original context data unless the compaction flow explicitly owns that behavior.
|
||||
- Preserve loaded skill name/revision metadata in summaries without copying full skill bodies.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ Rules:
|
|||
- Use terse bullet points, not prose
|
||||
- Collapse related items into single lines
|
||||
- Keep exact values: file paths, config values, code identifiers, credentials, URLs
|
||||
- Preserve loaded skill names and revisions from skill_instructions metadata, but do not copy full skill bodies
|
||||
- Omit anything that can be re-derived from context
|
||||
- Group by topic, not chronology
|
||||
- No meta-commentary about the summarization
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
- Keep placeholder names, include aliases, and template assumptions synchronized with prompt-loading code and extensions.
|
||||
- Prompt changes can alter agent behavior; keep edits narrow and intentional.
|
||||
- Maintain clear separation between core behavior prompts and profile/plugin-specific customization.
|
||||
- Framework summary prompts must preserve loaded skill name/revision metadata without copying full skill bodies.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ common args: action skill_name query file_path
|
|||
workflow:
|
||||
- action `search`: find candidate skills by keywords or trigger phrases from the current task
|
||||
- action `list`: discover available skills
|
||||
- action `load`: append one skill's full instructions to chat history by `skill_name`
|
||||
- action `load`: load one skill by `skill_name`
|
||||
- action `read_file`: open one file inside a loaded skill directory
|
||||
if the user says "find/search a skill", call `search` before `load` even when the likely skill name seems obvious
|
||||
`read_file` requires both `skill_name` and `file_path`; load the skill first, then read `SKILL.md` or the named relative file
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ You must return a single summary of all records
|
|||
# Expected output
|
||||
Your output will be a text of the summary
|
||||
Length of the text should be one paragraph, approximately 100 words
|
||||
If a tool result includes skill_instructions metadata, preserve the loaded skill name and revision in the summary, but do not copy the full skill body
|
||||
No intro
|
||||
No conclusion
|
||||
No formatting
|
||||
Only the summary text is returned
|
||||
Only the summary text is returned
|
||||
|
|
@ -8,8 +8,7 @@ You must return a single summary of all records
|
|||
Your output will be a text of the summary
|
||||
Summary must be shorter than original messages
|
||||
Length of the text should be maximum one paragraph, approximately 100 words, shorter if original is shorter
|
||||
If a tool result includes skill_instructions metadata, preserve the loaded skill name and revision in the summary, but do not copy the full skill body
|
||||
No intro
|
||||
No conclusion
|
||||
No formatting
|
||||
Only the summary text is returned
|
||||
Only the summary text is returned
|
||||
|
|
@ -38,7 +38,6 @@ class _FakeAgent:
|
|||
def __init__(self) -> None:
|
||||
self.data = {}
|
||||
self.context = types.SimpleNamespace(id="ctx")
|
||||
self.history = types.SimpleNamespace(output=lambda: [])
|
||||
|
||||
def read_prompt(self, _name: str, **kwargs) -> str:
|
||||
return f"deleted {kwargs.get('memory_count', 0)}"
|
||||
|
|
@ -75,10 +74,6 @@ def _load_skills_tool(monkeypatch, skill_root: Path):
|
|||
skills_stub.list_skills = lambda *args, **kwargs: [fake_skill]
|
||||
skills_stub.search_skills = lambda *args, **kwargs: [fake_skill]
|
||||
skills_stub.find_skill = lambda *args, **kwargs: fake_skill
|
||||
skills_stub.load_skill_for_agent = (
|
||||
lambda *args, **kwargs: "Skill: browser-form-workflows\n\nInstructions:\nUse labels before typing."
|
||||
)
|
||||
skills_stub.skill_revision = lambda skill_data: "rev1"
|
||||
monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
|
||||
|
||||
print_style_stub = types.ModuleType("helpers.print_style")
|
||||
|
|
@ -91,65 +86,6 @@ def _load_skills_tool(monkeypatch, skill_root: Path):
|
|||
return importlib.import_module("tools.skills_tool")
|
||||
|
||||
|
||||
class _FakeExtension:
|
||||
def __init__(self, agent=None):
|
||||
self.agent = agent
|
||||
|
||||
|
||||
class _FakeLoadedSkillAgent:
|
||||
def __init__(self) -> None:
|
||||
self.data = {"loaded_skills": ["browser-form-workflows"]}
|
||||
self.added_tool_results = []
|
||||
|
||||
def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
|
||||
content = {"tool_name": tool_name, "tool_result": tool_result, **kwargs}
|
||||
self.added_tool_results.append(content)
|
||||
return types.SimpleNamespace(
|
||||
output=lambda: [{"ai": False, "content": content}]
|
||||
)
|
||||
|
||||
|
||||
def _load_loaded_skills_extension(monkeypatch, skill_root: Path):
|
||||
extension_stub = types.ModuleType("helpers.extension")
|
||||
extension_stub.Extension = _FakeExtension
|
||||
monkeypatch.setitem(sys.modules, "helpers.extension", extension_stub)
|
||||
|
||||
agent_stub = types.ModuleType("agent")
|
||||
agent_stub.LoopData = lambda **kwargs: types.SimpleNamespace(**kwargs)
|
||||
monkeypatch.setitem(sys.modules, "agent", agent_stub)
|
||||
|
||||
skills_stub = types.ModuleType("helpers.skills")
|
||||
fake_skill = _FakeSkill(
|
||||
name="browser-form-workflows",
|
||||
description="Use for complex browser forms.",
|
||||
path=skill_root,
|
||||
tags=[],
|
||||
)
|
||||
skills_stub.find_skill = lambda *args, **kwargs: fake_skill
|
||||
skills_stub.load_skill_for_agent = (
|
||||
lambda *args, **kwargs: "Skill: browser-form-workflows\n\nInstructions:\nUse labels before typing."
|
||||
)
|
||||
skills_stub.skill_revision = lambda skill_data: "rev1"
|
||||
monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
|
||||
|
||||
tokens_stub = types.ModuleType("helpers.tokens")
|
||||
tokens_stub.approximate_tokens = lambda text: len(str(text).split())
|
||||
monkeypatch.setitem(sys.modules, "helpers.tokens", tokens_stub)
|
||||
|
||||
import helpers
|
||||
|
||||
monkeypatch.setattr(helpers, "skills", skills_stub, raising=False)
|
||||
monkeypatch.setattr(helpers, "tokens", tokens_stub, raising=False)
|
||||
|
||||
skills_tool_stub = types.ModuleType("tools.skills_tool")
|
||||
skills_tool_stub.DATA_NAME_LOADED_SKILLS = "loaded_skills"
|
||||
monkeypatch.setitem(sys.modules, "tools.skills_tool", skills_tool_stub)
|
||||
|
||||
module_name = "extensions.python.message_loop_prompts_after._65_include_loaded_skills"
|
||||
sys.modules.pop(module_name, None)
|
||||
return importlib.import_module(module_name)
|
||||
|
||||
|
||||
def _load_computer_use_remote_tool(monkeypatch):
|
||||
_install_tool_stub(monkeypatch)
|
||||
|
||||
|
|
@ -210,162 +146,6 @@ def test_skills_tool_accepts_action_alias_for_search(monkeypatch, tmp_path: Path
|
|||
assert "browser-form-workflows" in response.message
|
||||
|
||||
|
||||
def test_skills_tool_load_appends_skill_instructions_as_tool_result(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_skills_tool(monkeypatch, tmp_path)
|
||||
agent = _FakeAgent()
|
||||
tool = module.SkillsTool(
|
||||
agent,
|
||||
"skills_tool",
|
||||
None,
|
||||
{"action": "load", "skill_name": "browser-form-workflows"},
|
||||
"",
|
||||
None,
|
||||
)
|
||||
|
||||
response = asyncio.run(tool.execute(**tool.args))
|
||||
|
||||
assert "Skill: browser-form-workflows" in response.message
|
||||
assert response.additional["skill_instructions"]["name"] == "browser-form-workflows"
|
||||
assert response.additional["skill_instructions"]["content_included"] is True
|
||||
assert agent.data["loaded_skills"] == ["browser-form-workflows"]
|
||||
|
||||
|
||||
def test_skills_tool_load_omits_duplicate_visible_skill_revision(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_skills_tool(monkeypatch, tmp_path)
|
||||
agent = _FakeAgent()
|
||||
tool = module.SkillsTool(
|
||||
agent,
|
||||
"skills_tool",
|
||||
None,
|
||||
{"action": "load", "skill_name": "browser-form-workflows"},
|
||||
"",
|
||||
None,
|
||||
)
|
||||
first = asyncio.run(tool.execute(**tool.args))
|
||||
loaded_message = {
|
||||
"ai": False,
|
||||
"content": {"skill_instructions": first.additional["skill_instructions"]},
|
||||
}
|
||||
agent.history = types.SimpleNamespace(output=lambda: [loaded_message])
|
||||
|
||||
second = asyncio.run(tool.execute(**tool.args))
|
||||
|
||||
assert "already loaded in visible chat history" in second.message
|
||||
assert "Instructions:\nUse labels before typing." not in second.message
|
||||
assert second.additional["skill_instructions"]["content_included"] is False
|
||||
assert second.additional["skill_instructions"]["already_loaded"] is True
|
||||
assert agent.data["loaded_skills"] == ["browser-form-workflows"]
|
||||
|
||||
|
||||
def test_skills_tool_load_reloads_when_prior_skill_is_not_model_visible(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_skills_tool(monkeypatch, tmp_path)
|
||||
agent = _FakeAgent()
|
||||
tool = module.SkillsTool(
|
||||
agent,
|
||||
"skills_tool",
|
||||
None,
|
||||
{"action": "load", "skill_name": "browser-form-workflows"},
|
||||
"",
|
||||
None,
|
||||
)
|
||||
first = asyncio.run(tool.execute(**tool.args))
|
||||
hidden_message = types.SimpleNamespace(
|
||||
summary="",
|
||||
content={"skill_instructions": first.additional["skill_instructions"]},
|
||||
)
|
||||
agent.history = types.SimpleNamespace(
|
||||
all_messages=lambda: [hidden_message],
|
||||
output=lambda: [
|
||||
{
|
||||
"ai": False,
|
||||
"content": "Earlier history was summarized and no skill body is visible.",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
second = asyncio.run(tool.execute(**tool.args))
|
||||
|
||||
assert "Skill: browser-form-workflows" in second.message
|
||||
assert second.additional["skill_instructions"]["content_included"] is True
|
||||
|
||||
|
||||
def test_loaded_skills_extension_reattaches_missing_body_after_compaction(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_loaded_skills_extension(monkeypatch, tmp_path)
|
||||
agent = _FakeLoadedSkillAgent()
|
||||
loop_data = types.SimpleNamespace(
|
||||
extras_persistent={"loaded_skills": "legacy"},
|
||||
history_output=[
|
||||
{
|
||||
"ai": False,
|
||||
"content": "Earlier history was summarized and no skill body is visible.",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
|
||||
|
||||
assert "loaded_skills" not in loop_data.extras_persistent
|
||||
assert len(agent.added_tool_results) == 1
|
||||
added = agent.added_tool_results[0]
|
||||
assert added["tool_name"] == "skills_tool"
|
||||
assert "Skill: browser-form-workflows" in added["tool_result"]
|
||||
assert added["skill_instructions"] == {
|
||||
"name": "browser-form-workflows",
|
||||
"path": str(tmp_path),
|
||||
"revision": "rev1",
|
||||
"source": "skills_tool:reattach",
|
||||
"content_included": True,
|
||||
}
|
||||
assert loop_data.history_output[-1]["content"] == added
|
||||
|
||||
|
||||
def test_loaded_skills_extension_does_not_reattach_visible_revision(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_loaded_skills_extension(monkeypatch, tmp_path)
|
||||
agent = _FakeLoadedSkillAgent()
|
||||
loop_data = types.SimpleNamespace(
|
||||
extras_persistent={},
|
||||
history_output=[
|
||||
{
|
||||
"ai": False,
|
||||
"content": {
|
||||
"skill_instructions": {
|
||||
"name": "browser-form-workflows",
|
||||
"revision": "rev1",
|
||||
"content_included": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
|
||||
|
||||
assert agent.added_tool_results == []
|
||||
|
||||
|
||||
def test_loaded_skills_extension_keeps_reattachments_under_budget(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
module = _load_loaded_skills_extension(monkeypatch, tmp_path)
|
||||
monkeypatch.setattr(module, "SKILL_REATTACHMENT_TOKEN_BUDGET", 1)
|
||||
agent = _FakeLoadedSkillAgent()
|
||||
loop_data = types.SimpleNamespace(extras_persistent={}, history_output=[])
|
||||
|
||||
asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
|
||||
|
||||
assert agent.added_tool_results == []
|
||||
|
||||
|
||||
def test_skills_tool_read_file_action_reads_inside_skill_dir(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
|
|
|
|||
|
|
@ -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 self._load(skill_name)
|
||||
return Response(message=self._load(skill_name), break_loop=False)
|
||||
if action == "read_file":
|
||||
skill_name = self._normalize_skill_name(
|
||||
str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
|
||||
|
|
@ -185,14 +185,11 @@ class SkillsTool(Tool):
|
|||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def _load(self, skill_name: str) -> Response:
|
||||
def _load(self, skill_name: str) -> str:
|
||||
skill_name = self._normalize_skill_name(skill_name)
|
||||
|
||||
if not skill_name:
|
||||
return Response(
|
||||
message="Error: 'skill_name' is required for action=load.",
|
||||
break_loop=False,
|
||||
)
|
||||
return "Error: 'skill_name' is required for action=load."
|
||||
|
||||
# Verify skill exists
|
||||
skill = skills_helper.find_skill(
|
||||
|
|
@ -201,29 +198,9 @@ class SkillsTool(Tool):
|
|||
agent=self.agent,
|
||||
)
|
||||
if not skill:
|
||||
return Response(
|
||||
message=(
|
||||
f"Error: skill not found: {skill_name!r}. "
|
||||
"Try skills_tool action=list or action=search."
|
||||
),
|
||||
break_loop=False,
|
||||
)
|
||||
return f"Error: skill not found: {skill_name!r}. Try skills_tool action=list or action=search."
|
||||
|
||||
skill_data = skills_helper.load_skill_for_agent(
|
||||
skill_name=skill.name,
|
||||
agent=self.agent,
|
||||
)
|
||||
revision = skills_helper.skill_revision(skill_data)
|
||||
metadata = {
|
||||
"name": skill.name,
|
||||
"path": str(skill.path),
|
||||
"revision": revision,
|
||||
"source": "skills_tool:load",
|
||||
"content_included": True,
|
||||
}
|
||||
|
||||
# Keep the old ledger for UI/backwards compatibility. The skill body now
|
||||
# lives in normal tool-result history, not in per-turn prompt extras.
|
||||
# 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]
|
||||
|
|
@ -232,48 +209,7 @@ class SkillsTool(Tool):
|
|||
loaded.append(skill.name)
|
||||
self.agent.data[DATA_NAME_LOADED_SKILLS] = loaded[-max_loaded_skills():]
|
||||
|
||||
if self._visible_skill_revision_loaded(skill.name, revision):
|
||||
return Response(
|
||||
message=(
|
||||
f"Skill '{skill.name}' is already loaded in visible "
|
||||
"chat history for this revision."
|
||||
),
|
||||
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_revision_loaded(self, skill_name: str, revision: str) -> bool:
|
||||
history_obj = getattr(self.agent, "history", None)
|
||||
output = getattr(history_obj, "output", None)
|
||||
if not callable(output):
|
||||
return False
|
||||
|
||||
for message in output():
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
content = message.get("content")
|
||||
if not isinstance(content, dict):
|
||||
continue
|
||||
meta = content.get("skill_instructions")
|
||||
if not isinstance(meta, dict):
|
||||
continue
|
||||
if not meta.get("content_included"):
|
||||
continue
|
||||
if meta.get("name") == skill_name and meta.get("revision") == revision:
|
||||
return True
|
||||
return False
|
||||
return f"Loaded skill '{skill.name}' into EXTRAS."
|
||||
|
||||
def _read_file(self, skill_name: str, file_path: str) -> str:
|
||||
if not skill_name:
|
||||
|
|
|
|||
|
|
@ -25,15 +25,12 @@
|
|||
- 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(...)`.
|
||||
- `load` returns the full formatted skill instructions as the tool result so the instructions are appended once through normal message history instead of being reinjected through prompt extras.
|
||||
- `load` includes a `skill_instructions` metadata sidecar in the tool-result content with skill name, path, revision, source, and whether full content was included.
|
||||
- Duplicate `load` calls may omit the full body only when the same skill revision is still present in model-visible `history.output()` content.
|
||||
- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence, chat history content.
|
||||
- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state 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`, `skills_helper.load_skill_for_agent`, `skills_helper.skill_revision`, `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`, `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`.
|
||||
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
|
||||
|
||||
## Work Guidance
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue