mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-15 15:39:47 +00:00
Merge PR #2959: feat(skills): add disabled_skills config to exclude skills from loading
feat(skills): add disabled_skills config to exclude skills from loading
This commit is contained in:
commit
a81e4c1791
20
README.md
20
README.md
@ -1597,6 +1597,26 @@ When enabled, all incoming messages — regardless of which channel they arrive
|
|||||||
|
|
||||||
> This is designed for single-user, multi-device setups. It is **off by default** — existing users see zero behavior change.
|
> This is designed for single-user, multi-device setups. It is **off by default** — existing users see zero behavior change.
|
||||||
|
|
||||||
|
### Disabled Skills
|
||||||
|
|
||||||
|
nanobot ships with built-in skills, and your workspace can also define custom skills under `skills/`. If you want to hide specific skills from the agent, set `agents.defaults.disabledSkills` to a list of skill directory names:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"agents": {
|
||||||
|
"defaults": {
|
||||||
|
"disabledSkills": ["github", "weather"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Disabled skills are excluded from the main agent's skill summary, from always-on skill injection, and from subagent skill summaries. This is useful when some bundled skills are unnecessary for your deployment or should not be exposed to end users.
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------|---------|-------------|
|
||||||
|
| `agents.defaults.disabledSkills` | `[]` | List of skill directory names to exclude from loading. Applies to both built-in skills and workspace skills. |
|
||||||
|
|
||||||
## 🧩 Multiple Instances
|
## 🧩 Multiple Instances
|
||||||
|
|
||||||
Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
|
Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
|
||||||
|
|||||||
@ -22,11 +22,11 @@ class ContextBuilder:
|
|||||||
_MAX_RECENT_HISTORY = 50
|
_MAX_RECENT_HISTORY = 50
|
||||||
_RUNTIME_CONTEXT_END = "[/Runtime Context]"
|
_RUNTIME_CONTEXT_END = "[/Runtime Context]"
|
||||||
|
|
||||||
def __init__(self, workspace: Path, timezone: str | None = None):
|
def __init__(self, workspace: Path, timezone: str | None = None, disabled_skills: list[str] | None = None):
|
||||||
self.workspace = workspace
|
self.workspace = workspace
|
||||||
self.timezone = timezone
|
self.timezone = timezone
|
||||||
self.memory = MemoryStore(workspace)
|
self.memory = MemoryStore(workspace)
|
||||||
self.skills = SkillsLoader(workspace)
|
self.skills = SkillsLoader(workspace, disabled_skills=set(disabled_skills) if disabled_skills else None)
|
||||||
|
|
||||||
def build_system_prompt(
|
def build_system_prompt(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@ -152,6 +152,7 @@ class AgentLoop:
|
|||||||
session_ttl_minutes: int = 0,
|
session_ttl_minutes: int = 0,
|
||||||
hooks: list[AgentHook] | None = None,
|
hooks: list[AgentHook] | None = None,
|
||||||
unified_session: bool = False,
|
unified_session: bool = False,
|
||||||
|
disabled_skills: list[str] | None = None,
|
||||||
):
|
):
|
||||||
from nanobot.config.schema import ExecToolConfig, WebToolsConfig
|
from nanobot.config.schema import ExecToolConfig, WebToolsConfig
|
||||||
|
|
||||||
@ -184,7 +185,7 @@ class AgentLoop:
|
|||||||
self._last_usage: dict[str, int] = {}
|
self._last_usage: dict[str, int] = {}
|
||||||
self._extra_hooks: list[AgentHook] = hooks or []
|
self._extra_hooks: list[AgentHook] = hooks or []
|
||||||
|
|
||||||
self.context = ContextBuilder(workspace, timezone=timezone)
|
self.context = ContextBuilder(workspace, timezone=timezone, disabled_skills=disabled_skills)
|
||||||
self.sessions = session_manager or SessionManager(workspace)
|
self.sessions = session_manager or SessionManager(workspace)
|
||||||
self.tools = ToolRegistry()
|
self.tools = ToolRegistry()
|
||||||
self.runner = AgentRunner(provider)
|
self.runner = AgentRunner(provider)
|
||||||
@ -197,6 +198,7 @@ class AgentLoop:
|
|||||||
max_tool_result_chars=self.max_tool_result_chars,
|
max_tool_result_chars=self.max_tool_result_chars,
|
||||||
exec_config=self.exec_config,
|
exec_config=self.exec_config,
|
||||||
restrict_to_workspace=restrict_to_workspace,
|
restrict_to_workspace=restrict_to_workspace,
|
||||||
|
disabled_skills=disabled_skills,
|
||||||
)
|
)
|
||||||
self._unified_session = unified_session
|
self._unified_session = unified_session
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|||||||
@ -28,10 +28,11 @@ class SkillsLoader:
|
|||||||
specific tools or perform certain tasks.
|
specific tools or perform certain tasks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None):
|
def __init__(self, workspace: Path, builtin_skills_dir: Path | None = None, disabled_skills: set[str] | None = None):
|
||||||
self.workspace = workspace
|
self.workspace = workspace
|
||||||
self.workspace_skills = workspace / "skills"
|
self.workspace_skills = workspace / "skills"
|
||||||
self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR
|
self.builtin_skills = builtin_skills_dir or BUILTIN_SKILLS_DIR
|
||||||
|
self.disabled_skills = disabled_skills or set()
|
||||||
|
|
||||||
def _skill_entries_from_dir(self, base: Path, source: str, *, skip_names: set[str] | None = None) -> list[dict[str, str]]:
|
def _skill_entries_from_dir(self, base: Path, source: str, *, skip_names: set[str] | None = None) -> list[dict[str, str]]:
|
||||||
if not base.exists():
|
if not base.exists():
|
||||||
@ -66,6 +67,9 @@ class SkillsLoader:
|
|||||||
self._skill_entries_from_dir(self.builtin_skills, "builtin", skip_names=workspace_names)
|
self._skill_entries_from_dir(self.builtin_skills, "builtin", skip_names=workspace_names)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.disabled_skills:
|
||||||
|
skills = [s for s in skills if s["name"] not in self.disabled_skills]
|
||||||
|
|
||||||
if filter_unavailable:
|
if filter_unavailable:
|
||||||
return [skill for skill in skills if self._check_requirements(self._get_skill_meta(skill["name"]))]
|
return [skill for skill in skills if self._check_requirements(self._get_skill_meta(skill["name"]))]
|
||||||
return skills
|
return skills
|
||||||
|
|||||||
@ -52,6 +52,7 @@ class SubagentManager:
|
|||||||
web_config: "WebToolsConfig | None" = None,
|
web_config: "WebToolsConfig | None" = None,
|
||||||
exec_config: "ExecToolConfig | None" = None,
|
exec_config: "ExecToolConfig | None" = None,
|
||||||
restrict_to_workspace: bool = False,
|
restrict_to_workspace: bool = False,
|
||||||
|
disabled_skills: list[str] | None = None,
|
||||||
):
|
):
|
||||||
from nanobot.config.schema import ExecToolConfig
|
from nanobot.config.schema import ExecToolConfig
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ class SubagentManager:
|
|||||||
self.max_tool_result_chars = max_tool_result_chars
|
self.max_tool_result_chars = max_tool_result_chars
|
||||||
self.exec_config = exec_config or ExecToolConfig()
|
self.exec_config = exec_config or ExecToolConfig()
|
||||||
self.restrict_to_workspace = restrict_to_workspace
|
self.restrict_to_workspace = restrict_to_workspace
|
||||||
|
self.disabled_skills = set(disabled_skills or [])
|
||||||
self.runner = AgentRunner(provider)
|
self.runner = AgentRunner(provider)
|
||||||
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
self._running_tasks: dict[str, asyncio.Task[None]] = {}
|
||||||
self._session_tasks: dict[str, set[str]] = {} # session_key -> {task_id, ...}
|
self._session_tasks: dict[str, set[str]] = {} # session_key -> {task_id, ...}
|
||||||
@ -236,7 +238,10 @@ class SubagentManager:
|
|||||||
from nanobot.agent.skills import SkillsLoader
|
from nanobot.agent.skills import SkillsLoader
|
||||||
|
|
||||||
time_ctx = ContextBuilder._build_runtime_context(None, None)
|
time_ctx = ContextBuilder._build_runtime_context(None, None)
|
||||||
skills_summary = SkillsLoader(self.workspace).build_skills_summary()
|
skills_summary = SkillsLoader(
|
||||||
|
self.workspace,
|
||||||
|
disabled_skills=self.disabled_skills,
|
||||||
|
).build_skills_summary()
|
||||||
return render_template(
|
return render_template(
|
||||||
"agent/subagent_system.md",
|
"agent/subagent_system.md",
|
||||||
time_ctx=time_ctx,
|
time_ctx=time_ctx,
|
||||||
|
|||||||
@ -591,6 +591,7 @@ def serve(
|
|||||||
channels_config=runtime_config.channels,
|
channels_config=runtime_config.channels,
|
||||||
timezone=runtime_config.agents.defaults.timezone,
|
timezone=runtime_config.agents.defaults.timezone,
|
||||||
unified_session=runtime_config.agents.defaults.unified_session,
|
unified_session=runtime_config.agents.defaults.unified_session,
|
||||||
|
disabled_skills=runtime_config.agents.defaults.disabled_skills,
|
||||||
session_ttl_minutes=runtime_config.agents.defaults.session_ttl_minutes,
|
session_ttl_minutes=runtime_config.agents.defaults.session_ttl_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -684,6 +685,7 @@ def gateway(
|
|||||||
channels_config=config.channels,
|
channels_config=config.channels,
|
||||||
timezone=config.agents.defaults.timezone,
|
timezone=config.agents.defaults.timezone,
|
||||||
unified_session=config.agents.defaults.unified_session,
|
unified_session=config.agents.defaults.unified_session,
|
||||||
|
disabled_skills=config.agents.defaults.disabled_skills,
|
||||||
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -917,6 +919,7 @@ def agent(
|
|||||||
channels_config=config.channels,
|
channels_config=config.channels,
|
||||||
timezone=config.agents.defaults.timezone,
|
timezone=config.agents.defaults.timezone,
|
||||||
unified_session=config.agents.defaults.unified_session,
|
unified_session=config.agents.defaults.unified_session,
|
||||||
|
disabled_skills=config.agents.defaults.disabled_skills,
|
||||||
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
session_ttl_minutes=config.agents.defaults.session_ttl_minutes,
|
||||||
)
|
)
|
||||||
restart_notice = consume_restart_notice_from_env()
|
restart_notice = consume_restart_notice_from_env()
|
||||||
|
|||||||
@ -77,6 +77,7 @@ class AgentDefaults(Base):
|
|||||||
reasoning_effort: str | None = None # low / medium / high / adaptive - enables LLM thinking mode
|
reasoning_effort: str | None = None # low / medium / high / adaptive - enables LLM thinking mode
|
||||||
timezone: str = "UTC" # IANA timezone, e.g. "Asia/Shanghai", "America/New_York"
|
timezone: str = "UTC" # IANA timezone, e.g. "Asia/Shanghai", "America/New_York"
|
||||||
unified_session: bool = False # Share one session across all channels (single-user multi-device)
|
unified_session: bool = False # Share one session across all channels (single-user multi-device)
|
||||||
|
disabled_skills: list[str] = Field(default_factory=list) # Skill names to exclude from loading (e.g. ["summarize", "skill-creator"])
|
||||||
session_ttl_minutes: int = Field(
|
session_ttl_minutes: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
ge=0,
|
ge=0,
|
||||||
|
|||||||
@ -82,6 +82,7 @@ class Nanobot:
|
|||||||
mcp_servers=config.tools.mcp_servers,
|
mcp_servers=config.tools.mcp_servers,
|
||||||
timezone=defaults.timezone,
|
timezone=defaults.timezone,
|
||||||
unified_session=defaults.unified_session,
|
unified_session=defaults.unified_session,
|
||||||
|
disabled_skills=defaults.disabled_skills,
|
||||||
session_ttl_minutes=defaults.session_ttl_minutes,
|
session_ttl_minutes=defaults.session_ttl_minutes,
|
||||||
)
|
)
|
||||||
return cls(loop)
|
return cls(loop)
|
||||||
|
|||||||
@ -250,3 +250,63 @@ def test_list_skills_openclaw_metadata_parsed_for_requirements(
|
|||||||
assert entries == [
|
assert entries == [
|
||||||
{"name": "openclaw_skill", "path": str(skill_path), "source": "workspace"},
|
{"name": "openclaw_skill", "path": str(skill_path), "source": "workspace"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_skills_excluded_from_list(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
ws_skills = workspace / "skills"
|
||||||
|
ws_skills.mkdir(parents=True)
|
||||||
|
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||||
|
beta_path = _write_skill(ws_skills, "beta", body="# Beta")
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
builtin.mkdir()
|
||||||
|
|
||||||
|
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||||
|
entries = loader.list_skills(filter_unavailable=False)
|
||||||
|
assert len(entries) == 1
|
||||||
|
assert entries[0]["name"] == "beta"
|
||||||
|
assert entries[0]["path"] == str(beta_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_skills_empty_set_no_effect(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
ws_skills = workspace / "skills"
|
||||||
|
ws_skills.mkdir(parents=True)
|
||||||
|
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||||
|
_write_skill(ws_skills, "beta", body="# Beta")
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
builtin.mkdir()
|
||||||
|
|
||||||
|
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills=set())
|
||||||
|
entries = loader.list_skills(filter_unavailable=False)
|
||||||
|
assert len(entries) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_skills_excluded_from_build_skills_summary(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
ws_skills = workspace / "skills"
|
||||||
|
ws_skills.mkdir(parents=True)
|
||||||
|
_write_skill(ws_skills, "alpha", body="# Alpha")
|
||||||
|
_write_skill(ws_skills, "beta", body="# Beta")
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
builtin.mkdir()
|
||||||
|
|
||||||
|
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||||
|
summary = loader.build_skills_summary()
|
||||||
|
assert "alpha" not in summary
|
||||||
|
assert "beta" in summary
|
||||||
|
|
||||||
|
|
||||||
|
def test_disabled_skills_excluded_from_get_always_skills(tmp_path: Path) -> None:
|
||||||
|
workspace = tmp_path / "ws"
|
||||||
|
ws_skills = workspace / "skills"
|
||||||
|
ws_skills.mkdir(parents=True)
|
||||||
|
_write_skill(ws_skills, "alpha", metadata_json={"always": True}, body="# Alpha")
|
||||||
|
_write_skill(ws_skills, "beta", metadata_json={"always": True}, body="# Beta")
|
||||||
|
builtin = tmp_path / "builtin"
|
||||||
|
builtin.mkdir()
|
||||||
|
|
||||||
|
loader = SkillsLoader(workspace, builtin_skills_dir=builtin, disabled_skills={"alpha"})
|
||||||
|
always = loader.get_always_skills()
|
||||||
|
assert "alpha" not in always
|
||||||
|
assert "beta" in always
|
||||||
|
|||||||
@ -323,3 +323,27 @@ async def test_subagent_registers_grep_and_glob(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert "grep" in captured["tool_names"]
|
assert "grep" in captured["tool_names"]
|
||||||
assert "glob" in captured["tool_names"]
|
assert "glob" in captured["tool_names"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_subagent_prompt_respects_disabled_skills(tmp_path: Path) -> None:
|
||||||
|
bus = MessageBus()
|
||||||
|
provider = MagicMock()
|
||||||
|
provider.get_default_model.return_value = "test-model"
|
||||||
|
skills_dir = tmp_path / "skills"
|
||||||
|
(skills_dir / "alpha").mkdir(parents=True)
|
||||||
|
(skills_dir / "alpha" / "SKILL.md").write_text("# Alpha\n\nhidden\n", encoding="utf-8")
|
||||||
|
(skills_dir / "beta").mkdir(parents=True)
|
||||||
|
(skills_dir / "beta" / "SKILL.md").write_text("# Beta\n\nshown\n", encoding="utf-8")
|
||||||
|
|
||||||
|
mgr = SubagentManager(
|
||||||
|
provider=provider,
|
||||||
|
workspace=tmp_path,
|
||||||
|
bus=bus,
|
||||||
|
max_tool_result_chars=4096,
|
||||||
|
disabled_skills=["alpha"],
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = mgr._build_subagent_prompt()
|
||||||
|
|
||||||
|
assert "alpha" not in prompt
|
||||||
|
assert "beta" in prompt
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user