feat: add SkillManageTool for agent-authored procedural memory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tarun Suresh
2026-03-16 17:56:11 +00:00
co-authored by Claude Opus 4.6
parent 153435edb6
commit c1e01b1e8b
5 changed files with 274 additions and 1 deletions
+10
View File
@@ -1085,6 +1085,15 @@ class CompressionConfig:
strategy: str = "session_consolidation"
@dataclass(slots=True)
class SkillsConfig:
"""Configuration for agent-authored procedural skills."""
skills_dir: str = "~/.openjarvis/skills/"
nudge_interval: int = 15
auto_discover: bool = True
@dataclass
class JarvisConfig:
"""Top-level configuration for OpenJarvis."""
@@ -1112,6 +1121,7 @@ class JarvisConfig:
memory_files: MemoryFilesConfig = field(default_factory=MemoryFilesConfig)
system_prompt: SystemPromptConfig = field(default_factory=SystemPromptConfig)
compression: CompressionConfig = field(default_factory=CompressionConfig)
skills: SkillsConfig = field(default_factory=SkillsConfig)
@property
def memory(self) -> StorageConfig:
+15 -1
View File
@@ -104,4 +104,18 @@ def load_skill(
return manifest
__all__ = ["load_skill"]
def discover_skills(directory: str | Path) -> list[SkillManifest]:
"""Scan directory for TOML skill files and load them."""
directory = Path(directory).expanduser()
if not directory.exists():
return []
manifests = []
for toml_file in sorted(directory.glob("*.toml")):
try:
manifests.append(load_skill(toml_file))
except Exception:
continue
return manifests
__all__ = ["load_skill", "discover_skills"]
+5
View File
@@ -86,4 +86,9 @@ try:
except ImportError:
pass
try:
import openjarvis.tools.skill_manage # noqa: F401
except ImportError:
pass
__all__ = ["BaseTool", "ToolExecutor", "ToolSpec"]
+157
View File
@@ -0,0 +1,157 @@
"""SkillManageTool — create, list, load, or delete agent-authored skills."""
from __future__ import annotations
from pathlib import Path
from typing import Any, List
from openjarvis.core.registry import ToolRegistry
from openjarvis.core.types import ToolResult
from openjarvis.tools._stubs import BaseTool, ToolSpec
@ToolRegistry.register("skill_manage")
class SkillManageTool(BaseTool):
"""Manage agent-authored procedural skills."""
def __init__(self, skills_dir: Path | str = "~/.openjarvis/skills/") -> None:
self._skills_dir = Path(skills_dir).expanduser()
@property
def spec(self) -> ToolSpec:
return ToolSpec(
name="skill_manage",
description="Create, list, load, or delete agent-authored skills.",
parameters={
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["create", "list", "load", "delete"],
"description": "Action to perform.",
},
"name": {
"type": "string",
"description": "Skill name (for create/load/delete).",
},
"description": {
"type": "string",
"description": "Skill description (for create).",
},
"steps": {
"type": "array",
"description": (
"List of step dicts with tool_name and optional"
" arguments_template (for create)."
),
},
},
"required": ["action"],
},
category="skill",
)
def execute(self, **params: Any) -> ToolResult:
action = params.get("action", "list")
name = params.get("name", "")
if action == "create":
return self._create(
name, params.get("description", ""), params.get("steps", [])
)
elif action == "list":
return self._list()
elif action == "load":
return self._load(name)
elif action == "delete":
return self._delete(name)
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Unknown action: {action}",
)
def _create(
self, name: str, description: str, steps: List[dict]
) -> ToolResult:
if not name:
return ToolResult(
tool_name=self.spec.name,
success=False,
content="Skill name is required.",
)
self._skills_dir.mkdir(parents=True, exist_ok=True)
path = self._skills_dir / f"{name}.toml"
lines = [
"[skill]",
f'name = "{name}"',
f'description = "{description}"',
"",
]
for step in steps:
lines.append("[[skill.steps]]")
lines.append(f'tool_name = "{step.get("tool_name", "")}"')
if "arguments_template" in step:
lines.append(
f"arguments_template = '{step['arguments_template']}'"
)
if "output_key" in step:
lines.append(f'output_key = "{step["output_key"]}"')
lines.append("")
path.write_text("\n".join(lines))
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Created skill: {name}",
)
def _list(self) -> ToolResult:
if not self._skills_dir.exists():
return ToolResult(
tool_name=self.spec.name,
success=True,
content="No skills directory found.",
)
skills = []
for f in sorted(self._skills_dir.glob("*.toml")):
skills.append(f.stem)
if not skills:
return ToolResult(
tool_name=self.spec.name,
success=True,
content="No skills found.",
)
return ToolResult(
tool_name=self.spec.name,
success=True,
content="Available skills:\n"
+ "\n".join(f"- {s}" for s in skills),
)
def _load(self, name: str) -> ToolResult:
path = self._skills_dir / f"{name}.toml"
if not path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Skill not found: {name}",
)
return ToolResult(
tool_name=self.spec.name,
success=True,
content=path.read_text(),
)
def _delete(self, name: str) -> ToolResult:
path = self._skills_dir / f"{name}.toml"
if not path.exists():
return ToolResult(
tool_name=self.spec.name,
success=False,
content=f"Skill not found: {name}",
)
path.unlink()
return ToolResult(
tool_name=self.spec.name,
success=True,
content=f"Deleted skill: {name}",
)
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
import pytest
from pathlib import Path
@pytest.fixture
def skills_dir(tmp_path: Path) -> Path:
d = tmp_path / "skills"
d.mkdir()
return d
def test_skill_create(skills_dir: Path):
from openjarvis.tools.skill_manage import SkillManageTool
tool = SkillManageTool(skills_dir=skills_dir)
result = tool.execute(
action="create",
name="api_health",
description="Check API health",
steps=[
{
"tool_name": "http_request",
"arguments_template": '{"url": "{endpoint}/health"}',
}
],
)
assert result.success
assert (skills_dir / "api_health.toml").exists()
def test_skill_list(skills_dir: Path):
from openjarvis.tools.skill_manage import SkillManageTool
tool = SkillManageTool(skills_dir=skills_dir)
tool.execute(
action="create",
name="skill_a",
description="Skill A",
steps=[{"tool_name": "calculator"}],
)
tool.execute(
action="create",
name="skill_b",
description="Skill B",
steps=[{"tool_name": "calculator"}],
)
result = tool.execute(action="list")
assert "skill_a" in result.content
assert "skill_b" in result.content
def test_skill_delete(skills_dir: Path):
from openjarvis.tools.skill_manage import SkillManageTool
tool = SkillManageTool(skills_dir=skills_dir)
tool.execute(
action="create",
name="temp_skill",
description="Temp",
steps=[{"tool_name": "calculator"}],
)
assert (skills_dir / "temp_skill.toml").exists()
result = tool.execute(action="delete", name="temp_skill")
assert result.success
assert not (skills_dir / "temp_skill.toml").exists()
def test_skill_load(skills_dir: Path):
from openjarvis.tools.skill_manage import SkillManageTool
tool = SkillManageTool(skills_dir=skills_dir)
tool.execute(
action="create",
name="my_skill",
description="My skill desc",
steps=[
{
"tool_name": "web_search",
"arguments_template": '{"q": "test"}',
}
],
)
result = tool.execute(action="load", name="my_skill")
assert "web_search" in result.content
assert "My skill desc" in result.content