Revert loaded skill history persistence

Reverts db01d7c1c873f59b656618bb23890bb518d4c0a3 and 3c83b2eca290f824867cb51cbed3fec60de2a8cb. Restores the prior loaded-skills prompt-extras behavior and removes the compaction reattachment metadata path.

Alessandro committed Jun 18, 2026 at 16:45 UTC bf2741990a239ce68877ca8565f5fc842e9ad40b
13 files changed +32 -389
extensions/python/message_loop_prompts_after/AGENTS.md
+2 -4
@@ -2,19 +2,17 @@
2
3 ## Purpose
4
5 -- Own prompt extras and history-output adjustments appended after primary message-loop prompt construction.
5 +- Own prompt extras appended after primary message-loop prompt construction.
6
7 ## Ownership
8
9 -- Ordered Python files own current datetime, skill recall/load context, loaded-skill reattachment, agent info, parallel job status, and workdir extras injection.
9 +- Ordered Python files own current datetime, skill recall/load context, agent info, parallel job status, and workdir extras injection.
10
11 ## Local Contracts
12
13 - Keep injected content bounded and clearly attributed.
14 - Preserve ordering where later prompt extras depend on earlier recall or load results.
15 - Do not expose secrets or private files from workdir extras.
16 -- 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.
17 -- If compression hides an explicitly loaded skill body, this hook may reattach the missing visible revision as a bounded normal tool-result history message.
16
17 ## Work Guidance
18
extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
+17 -74
@@ -1,95 +1,38 @@
1 from helpers.extension import Extension
2 -from helpers import skills, tokens
2 +from helpers import skills
3 from tools.skills_tool import DATA_NAME_LOADED_SKILLS
4 from agent import LoopData
5
6
7 -SKILL_REATTACHMENT_TOKEN_BUDGET = 12_000
8 -SKILL_REATTACHMENT_HEADER = (
9 - "Reattached loaded skill instructions after history compaction."
10 -)
11 -
12 -
7 class IncludeLoadedSkills(Extension):
8 async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
9 if not self.agent:
10 return
11
18 - loop_data.extras_persistent.pop("loaded_skills", None)
12 + extras = loop_data.extras_persistent
13 +
14 + # Get loaded skills names
15 skill_names = self.agent.data.get(DATA_NAME_LOADED_SKILLS)
16 if not skill_names:
17 return
18
23 - # `skills_tool load` now appends full skill instructions as a normal
24 - # tool-result history message. Keep this legacy ledger pruned, but do
25 - # not reinject loaded skills through prompt extras every turn.
19 + # load skill text here
20 + content = ""
21 visible_skill_names = []
27 - loaded_skills = []
22 for skill_name in skill_names:
29 - skill = skills.find_skill(skill_name, agent=self.agent)
30 - if not skill:
23 + if not skills.find_skill(skill_name, agent=self.agent):
24 continue
32 - visible_skill_names.append(skill.name)
33 - loaded_skills.append(skill)
25 + visible_skill_names.append(skill_name)
26 + skill_data = skills.load_skill_for_agent(skill_name=skill_name, agent=self.agent)
27 + content += "\n\n" + skill_data
28 self.agent.data[DATA_NAME_LOADED_SKILLS] = visible_skill_names
35 -
36 - self._reattach_missing_skill_bodies(loop_data, loaded_skills)
37 -
38 - def _reattach_missing_skill_bodies(self, loop_data: LoopData, loaded_skills):
39 - if not self.agent or not loaded_skills:
29 + content = content.strip()
30 + if not content:
31 return
32
42 - visible_revisions = _visible_skill_revisions(loop_data.history_output)
43 - selected = []
44 - used_tokens = 0
45 -
46 - for skill in reversed(loaded_skills):
47 - skill_data = skills.load_skill_for_agent(
48 - skill_name=skill.name,
49 - agent=self.agent,
50 - )
51 - revision = skills.skill_revision(skill_data)
52 - if (skill.name, revision) in visible_revisions:
53 - continue
54 -
55 - message = f"{SKILL_REATTACHMENT_HEADER}\n\n{skill_data}"
56 - message_tokens = tokens.approximate_tokens(message)
57 - if used_tokens + message_tokens > SKILL_REATTACHMENT_TOKEN_BUDGET:
58 - continue
59 -
60 - selected.append((skill, revision, message))
61 - used_tokens += message_tokens
62 -
63 - for skill, revision, message in reversed(selected):
64 - history_message = self.agent.hist_add_tool_result(
65 - "skills_tool",
66 - message,
67 - skill_instructions={
68 - "name": skill.name,
69 - "path": str(skill.path),
70 - "revision": revision,
71 - "source": "skills_tool:reattach",
72 - "content_included": True,
73 - },
74 - )
75 - loop_data.history_output.extend(history_message.output())
76 -
33
78 -def _visible_skill_revisions(history_output) -> set[tuple[str, str]]:
79 - visible = set()
80 - for message in history_output or []:
81 - if not isinstance(message, dict):
82 - continue
83 - content = message.get("content")
84 - if not isinstance(content, dict):
85 - continue
86 - meta = content.get("skill_instructions")
87 - if not isinstance(meta, dict):
88 - continue
89 - if not meta.get("content_included"):
90 - continue
91 - name = str(meta.get("name") or "").strip()
92 - revision = str(meta.get("revision") or "").strip()
93 - if name and revision:
94 - visible.add((name, revision))
95 - return visible
34 + # Inject into extras
35 + extras["loaded_skills"] = self.agent.read_prompt(
36 + "agent.system.skills.loaded.md",
37 + skills=content,
38 + )
helpers/skills.py
-5
@@ -1,6 +1,5 @@
1 from __future__ import annotations
2
3 -import hashlib
3 import os
4 import re
5 from dataclasses import dataclass, field
@@ -452,10 +451,6 @@ def load_skill_for_agent(
451 return "\n".join(lines)
452
453
455 -def skill_revision(skill_data: str) -> str:
456 - return hashlib.sha256(skill_data.encode("utf-8")).hexdigest()[:16]
457 -
458 -
454 def _get_skill_files(skill_dir: Path) -> str:
455 """Get file tree for skill directory."""
456 if not skill_dir.exists():
helpers/skills.py.dox.md
+2 -3
@@ -30,7 +30,6 @@
30 - `delete_skill(skill_path: str) -> None`: Delete a skill directory.
31 - `find_skill(skill_name: str, agent: Agent | None=..., include_content: bool=..., include_hidden: bool=...) -> Optional[Skill]`
32 - `load_skill_for_agent(skill_name: str, agent: Agent | None=...) -> str`: Load skill and format it as a complete string for agent context.
33 -- `skill_revision(skill_data: str) -> str`
33 - `_get_skill_files(skill_dir: Path) -> str`: Get file tree for skill directory.
34 - `search_skills(query: str, limit: int=..., agent: Agent | None=..., include_hidden: bool=...) -> List[Skill]`
35 - `validate_skill(skill: Skill) -> List[str]`
@@ -53,11 +52,11 @@
52 - Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
53 - Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
54 - Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, secret handling.
56 -- Imported dependency areas include: `__future__`, `dataclasses`, `hashlib`, `helpers`, `os`, `pathlib`, `re`, `typing`.
55 +- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
56
57 ## Key Concepts
58
60 -- 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`.
59 +- 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`.
60 - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
61
62 ## Work Guidance
plugins/_chat_compaction/AGENTS.md
-1
@@ -18,7 +18,6 @@
18 - Preserve chat history integrity and persistence after compaction.
19 - Keep generated summaries bounded by configured model and token limits.
20 - Do not discard original context data unless the compaction flow explicitly owns that behavior.
21 -- Preserve loaded skill name/revision metadata in summaries without copying full skill bodies.
21
22 ## Work Guidance
23
plugins/_chat_compaction/prompts/compact.sys.md
-1
@@ -6,7 +6,6 @@ Rules:
6 - Use terse bullet points, not prose
7 - Collapse related items into single lines
8 - Keep exact values: file paths, config values, code identifiers, credentials, URLs
9 -- Preserve loaded skill names and revisions from skill_instructions metadata, but do not copy full skill bodies
9 - Omit anything that can be re-derived from context
10 - Group by topic, not chronology
11 - No meta-commentary about the summarization
prompts/AGENTS.md
-1
@@ -18,7 +18,6 @@
18 - Keep placeholder names, include aliases, and template assumptions synchronized with prompt-loading code and extensions.
19 - Prompt changes can alter agent behavior; keep edits narrow and intentional.
20 - Maintain clear separation between core behavior prompts and profile/plugin-specific customization.
21 -- Framework summary prompts must preserve loaded skill name/revision metadata without copying full skill bodies.
21
22 ## Work Guidance
23
prompts/agent.system.tool.skills.md
+1 -1
@@ -5,7 +5,7 @@ common args: action skill_name query file_path
5 workflow:
6 - action `search`: find candidate skills by keywords or trigger phrases from the current task
7 - action `list`: discover available skills
8 -- action `load`: append one skill's full instructions to chat history by `skill_name`
8 +- action `load`: load one skill by `skill_name`
9 - action `read_file`: open one file inside a loaded skill directory
10 if the user says "find/search a skill", call `search` before `load` even when the likely skill name seems obvious
11 `read_file` requires both `skill_name` and `file_path`; load the skill first, then read `SKILL.md` or the named relative file
prompts/fw.bulk_summary.sys.md
+1 -2
@@ -7,8 +7,7 @@ You must return a single summary of all records
7 # Expected output
8 Your output will be a text of the summary
9 Length of the text should be one paragraph, approximately 100 words
10 -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
10 No intro
11 No conclusion
12 No formatting
14 -Only the summary text is returned
13 +Only the summary text is returned
\ No newline at end of file
prompts/fw.topic_summary.sys.md
+1 -2
@@ -8,8 +8,7 @@ You must return a single summary of all records
8 Your output will be a text of the summary
9 Summary must be shorter than original messages
10 Length of the text should be maximum one paragraph, approximately 100 words, shorter if original is shorter
11 -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
11 No intro
12 No conclusion
13 No formatting
15 -Only the summary text is returned
14 +Only the summary text is returned
\ No newline at end of file
tests/test_tool_action_contracts.py
-220
@@ -38,7 +38,6 @@ class _FakeAgent:
38 def __init__(self) -> None:
39 self.data = {}
40 self.context = types.SimpleNamespace(id="ctx")
41 - self.history = types.SimpleNamespace(output=lambda: [])
41
42 def read_prompt(self, _name: str, **kwargs) -> str:
43 return f"deleted {kwargs.get('memory_count', 0)}"
@@ -75,10 +74,6 @@ def _load_skills_tool(monkeypatch, skill_root: Path):
74 skills_stub.list_skills = lambda *args, **kwargs: [fake_skill]
75 skills_stub.search_skills = lambda *args, **kwargs: [fake_skill]
76 skills_stub.find_skill = lambda *args, **kwargs: fake_skill
78 - skills_stub.load_skill_for_agent = (
79 - lambda *args, **kwargs: "Skill: browser-form-workflows\n\nInstructions:\nUse labels before typing."
80 - )
81 - skills_stub.skill_revision = lambda skill_data: "rev1"
77 monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
78
79 print_style_stub = types.ModuleType("helpers.print_style")
@@ -91,65 +86,6 @@ def _load_skills_tool(monkeypatch, skill_root: Path):
86 return importlib.import_module("tools.skills_tool")
87
88
94 -class _FakeExtension:
95 - def __init__(self, agent=None):
96 - self.agent = agent
97 -
98 -
99 -class _FakeLoadedSkillAgent:
100 - def __init__(self) -> None:
101 - self.data = {"loaded_skills": ["browser-form-workflows"]}
102 - self.added_tool_results = []
103 -
104 - def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
105 - content = {"tool_name": tool_name, "tool_result": tool_result, **kwargs}
106 - self.added_tool_results.append(content)
107 - return types.SimpleNamespace(
108 - output=lambda: [{"ai": False, "content": content}]
109 - )
110 -
111 -
112 -def _load_loaded_skills_extension(monkeypatch, skill_root: Path):
113 - extension_stub = types.ModuleType("helpers.extension")
114 - extension_stub.Extension = _FakeExtension
115 - monkeypatch.setitem(sys.modules, "helpers.extension", extension_stub)
116 -
117 - agent_stub = types.ModuleType("agent")
118 - agent_stub.LoopData = lambda **kwargs: types.SimpleNamespace(**kwargs)
119 - monkeypatch.setitem(sys.modules, "agent", agent_stub)
120 -
121 - skills_stub = types.ModuleType("helpers.skills")
122 - fake_skill = _FakeSkill(
123 - name="browser-form-workflows",
124 - description="Use for complex browser forms.",
125 - path=skill_root,
126 - tags=[],
127 - )
128 - skills_stub.find_skill = lambda *args, **kwargs: fake_skill
129 - skills_stub.load_skill_for_agent = (
130 - lambda *args, **kwargs: "Skill: browser-form-workflows\n\nInstructions:\nUse labels before typing."
131 - )
132 - skills_stub.skill_revision = lambda skill_data: "rev1"
133 - monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
134 -
135 - tokens_stub = types.ModuleType("helpers.tokens")
136 - tokens_stub.approximate_tokens = lambda text: len(str(text).split())
137 - monkeypatch.setitem(sys.modules, "helpers.tokens", tokens_stub)
138 -
139 - import helpers
140 -
141 - monkeypatch.setattr(helpers, "skills", skills_stub, raising=False)
142 - monkeypatch.setattr(helpers, "tokens", tokens_stub, raising=False)
143 -
144 - skills_tool_stub = types.ModuleType("tools.skills_tool")
145 - skills_tool_stub.DATA_NAME_LOADED_SKILLS = "loaded_skills"
146 - monkeypatch.setitem(sys.modules, "tools.skills_tool", skills_tool_stub)
147 -
148 - module_name = "extensions.python.message_loop_prompts_after._65_include_loaded_skills"
149 - sys.modules.pop(module_name, None)
150 - return importlib.import_module(module_name)
151 -
152 -
89 def _load_computer_use_remote_tool(monkeypatch):
90 _install_tool_stub(monkeypatch)
91
@@ -210,162 +146,6 @@ def test_skills_tool_accepts_action_alias_for_search(monkeypatch, tmp_path: Path
146 assert "browser-form-workflows" in response.message
147
148
213 -def test_skills_tool_load_appends_skill_instructions_as_tool_result(
214 - monkeypatch, tmp_path: Path
215 -):
216 - module = _load_skills_tool(monkeypatch, tmp_path)
217 - agent = _FakeAgent()
218 - tool = module.SkillsTool(
219 - agent,
220 - "skills_tool",
221 - None,
222 - {"action": "load", "skill_name": "browser-form-workflows"},
223 - "",
224 - None,
225 - )
226 -
227 - response = asyncio.run(tool.execute(**tool.args))
228 -
229 - assert "Skill: browser-form-workflows" in response.message
230 - assert response.additional["skill_instructions"]["name"] == "browser-form-workflows"
231 - assert response.additional["skill_instructions"]["content_included"] is True
232 - assert agent.data["loaded_skills"] == ["browser-form-workflows"]
233 -
234 -
235 -def test_skills_tool_load_omits_duplicate_visible_skill_revision(
236 - monkeypatch, tmp_path: Path
237 -):
238 - module = _load_skills_tool(monkeypatch, tmp_path)
239 - agent = _FakeAgent()
240 - tool = module.SkillsTool(
241 - agent,
242 - "skills_tool",
243 - None,
244 - {"action": "load", "skill_name": "browser-form-workflows"},
245 - "",
246 - None,
247 - )
248 - first = asyncio.run(tool.execute(**tool.args))
249 - loaded_message = {
250 - "ai": False,
251 - "content": {"skill_instructions": first.additional["skill_instructions"]},
252 - }
253 - agent.history = types.SimpleNamespace(output=lambda: [loaded_message])
254 -
255 - second = asyncio.run(tool.execute(**tool.args))
256 -
257 - assert "already loaded in visible chat history" in second.message
258 - assert "Instructions:\nUse labels before typing." not in second.message
259 - assert second.additional["skill_instructions"]["content_included"] is False
260 - assert second.additional["skill_instructions"]["already_loaded"] is True
261 - assert agent.data["loaded_skills"] == ["browser-form-workflows"]
262 -
263 -
264 -def test_skills_tool_load_reloads_when_prior_skill_is_not_model_visible(
265 - monkeypatch, tmp_path: Path
266 -):
267 - module = _load_skills_tool(monkeypatch, tmp_path)
268 - agent = _FakeAgent()
269 - tool = module.SkillsTool(
270 - agent,
271 - "skills_tool",
272 - None,
273 - {"action": "load", "skill_name": "browser-form-workflows"},
274 - "",
275 - None,
276 - )
277 - first = asyncio.run(tool.execute(**tool.args))
278 - hidden_message = types.SimpleNamespace(
279 - summary="",
280 - content={"skill_instructions": first.additional["skill_instructions"]},
281 - )
282 - agent.history = types.SimpleNamespace(
283 - all_messages=lambda: [hidden_message],
284 - output=lambda: [
285 - {
286 - "ai": False,
287 - "content": "Earlier history was summarized and no skill body is visible.",
288 - }
289 - ],
290 - )
291 -
292 - second = asyncio.run(tool.execute(**tool.args))
293 -
294 - assert "Skill: browser-form-workflows" in second.message
295 - assert second.additional["skill_instructions"]["content_included"] is True
296 -
297 -
298 -def test_loaded_skills_extension_reattaches_missing_body_after_compaction(
299 - monkeypatch, tmp_path: Path
300 -):
301 - module = _load_loaded_skills_extension(monkeypatch, tmp_path)
302 - agent = _FakeLoadedSkillAgent()
303 - loop_data = types.SimpleNamespace(
304 - extras_persistent={"loaded_skills": "legacy"},
305 - history_output=[
306 - {
307 - "ai": False,
308 - "content": "Earlier history was summarized and no skill body is visible.",
309 - }
310 - ],
311 - )
312 -
313 - asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
314 -
315 - assert "loaded_skills" not in loop_data.extras_persistent
316 - assert len(agent.added_tool_results) == 1
317 - added = agent.added_tool_results[0]
318 - assert added["tool_name"] == "skills_tool"
319 - assert "Skill: browser-form-workflows" in added["tool_result"]
320 - assert added["skill_instructions"] == {
321 - "name": "browser-form-workflows",
322 - "path": str(tmp_path),
323 - "revision": "rev1",
324 - "source": "skills_tool:reattach",
325 - "content_included": True,
326 - }
327 - assert loop_data.history_output[-1]["content"] == added
328 -
329 -
330 -def test_loaded_skills_extension_does_not_reattach_visible_revision(
331 - monkeypatch, tmp_path: Path
332 -):
333 - module = _load_loaded_skills_extension(monkeypatch, tmp_path)
334 - agent = _FakeLoadedSkillAgent()
335 - loop_data = types.SimpleNamespace(
336 - extras_persistent={},
337 - history_output=[
338 - {
339 - "ai": False,
340 - "content": {
341 - "skill_instructions": {
342 - "name": "browser-form-workflows",
343 - "revision": "rev1",
344 - "content_included": True,
345 - }
346 - },
347 - }
348 - ],
349 - )
350 -
351 - asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
352 -
353 - assert agent.added_tool_results == []
354 -
355 -
356 -def test_loaded_skills_extension_keeps_reattachments_under_budget(
357 - monkeypatch, tmp_path: Path
358 -):
359 - module = _load_loaded_skills_extension(monkeypatch, tmp_path)
360 - monkeypatch.setattr(module, "SKILL_REATTACHMENT_TOKEN_BUDGET", 1)
361 - agent = _FakeLoadedSkillAgent()
362 - loop_data = types.SimpleNamespace(extras_persistent={}, history_output=[])
363 -
364 - asyncio.run(module.IncludeLoadedSkills(agent).execute(loop_data))
365 -
366 - assert agent.added_tool_results == []
367 -
368 -
149 def test_skills_tool_read_file_action_reads_inside_skill_dir(
150 monkeypatch, tmp_path: Path
151 ):
tools/skills_tool.py
+6 -70
@@ -111,7 +111,7 @@ class SkillsTool(Tool):
111 skill_name = self._normalize_skill_name(
112 str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
113 )
114 - return self._load(skill_name)
114 + return Response(message=self._load(skill_name), break_loop=False)
115 if action == "read_file":
116 skill_name = self._normalize_skill_name(
117 str(kwargs.get("skill_name") or self.args.get("skill_name") or "")
@@ -185,14 +185,11 @@ class SkillsTool(Tool):
185 )
186 return "\n".join(lines)
187
188 - def _load(self, skill_name: str) -> Response:
188 + def _load(self, skill_name: str) -> str:
189 skill_name = self._normalize_skill_name(skill_name)
190
191 if not skill_name:
192 - return Response(
193 - message="Error: 'skill_name' is required for action=load.",
194 - break_loop=False,
195 - )
192 + return "Error: 'skill_name' is required for action=load."
193
194 # Verify skill exists
195 skill = skills_helper.find_skill(
@@ -201,29 +198,9 @@ class SkillsTool(Tool):
198 agent=self.agent,
199 )
200 if not skill:
204 - return Response(
205 - message=(
206 - f"Error: skill not found: {skill_name!r}. "
207 - "Try skills_tool action=list or action=search."
208 - ),
209 - break_loop=False,
210 - )
201 + return f"Error: skill not found: {skill_name!r}. Try skills_tool action=list or action=search."
202
212 - skill_data = skills_helper.load_skill_for_agent(
213 - skill_name=skill.name,
214 - agent=self.agent,
215 - )
216 - revision = skills_helper.skill_revision(skill_data)
217 - metadata = {
218 - "name": skill.name,
219 - "path": str(skill.path),
220 - "revision": revision,
221 - "source": "skills_tool:load",
222 - "content_included": True,
223 - }
224 -
225 - # Keep the old ledger for UI/backwards compatibility. The skill body now
226 - # lives in normal tool-result history, not in per-turn prompt extras.
203 + # Store skill name for fresh loading each turn
204 if not self.agent.data.get(DATA_NAME_LOADED_SKILLS):
205 self.agent.data[DATA_NAME_LOADED_SKILLS] = []
206 loaded = self.agent.data[DATA_NAME_LOADED_SKILLS]
@@ -232,48 +209,7 @@ class SkillsTool(Tool):
209 loaded.append(skill.name)
210 self.agent.data[DATA_NAME_LOADED_SKILLS] = loaded[-max_loaded_skills():]
211
235 - if self._visible_skill_revision_loaded(skill.name, revision):
236 - return Response(
237 - message=(
238 - f"Skill '{skill.name}' is already loaded in visible "
239 - "chat history for this revision."
240 - ),
241 - break_loop=False,
242 - additional={
243 - "skill_instructions": {
244 - **metadata,
245 - "content_included": False,
246 - "already_loaded": True,
247 - }
248 - },
249 - )
250 -
251 - return Response(
252 - message=skill_data,
253 - break_loop=False,
254 - additional={"skill_instructions": metadata},
255 - )
256 -
257 - def _visible_skill_revision_loaded(self, skill_name: str, revision: str) -> bool:
258 - history_obj = getattr(self.agent, "history", None)
259 - output = getattr(history_obj, "output", None)
260 - if not callable(output):
261 - return False
262 -
263 - for message in output():
264 - if not isinstance(message, dict):
265 - continue
266 - content = message.get("content")
267 - if not isinstance(content, dict):
268 - continue
269 - meta = content.get("skill_instructions")
270 - if not isinstance(meta, dict):
271 - continue
272 - if not meta.get("content_included"):
273 - continue
274 - if meta.get("name") == skill_name and meta.get("revision") == revision:
275 - return True
276 - return False
212 + return f"Loaded skill '{skill.name}' into EXTRAS."
213
214 def _read_file(self, skill_name: str, file_path: str) -> str:
215 if not skill_name:
tools/skills_tool.py.dox.md
+2 -5
@@ -25,15 +25,12 @@
25 - Update this file whenever tool arguments, output shape, `break_loop` behavior, intervention handling, prompt instructions, or side effects change.
26 - `SkillsTool` is a `Tool`.
27 - `SkillsTool` defines `execute(...)`.
28 -- `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.
29 -- `load` includes a `skill_instructions` metadata sidecar in the tool-result content with skill name, path, revision, source, and whether full content was included.
30 -- Duplicate `load` calls may omit the full body only when the same skill revision is still present in model-visible `history.output()` content.
31 -- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence, chat history content.
28 +- Observed side-effect areas: filesystem reads, filesystem deletion, settings/state persistence.
29 - Imported dependency areas include: `__future__`, `helpers`, `helpers.print_style`, `helpers.tool`, `pathlib`, `typing`.
30
31 ## Key Concepts
32
36 -- 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`.
33 +- 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`.
34 - Keep request/response, tool, or helper semantics documented here at the same time as source changes.
35
36 ## Work Guidance