mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-08-21 06:15:11 +00:00
Keep capability discovery aligned with runtime
Humanize MCP catalog labels with their server names while preserving canonical IDs. Suppress skill discovery prompts when skills_tool is unavailable and move the behaviour_adjustment prompt under the Memory plugin so disabling its implementation also removes its instructions.
This commit is contained in:
parent
a4f7a1352a
commit
dbe87ee184
10 changed files with 120 additions and 8 deletions
|
|
@ -17,6 +17,8 @@
|
|||
- Preserve ordering where later prompt extras depend on earlier recall or load results.
|
||||
- Do not expose secrets or private files from workdir extras.
|
||||
- Relevant-skill recall should search the raw user message when available, not the rendered history wrapper.
|
||||
- Relevant-skill hints must not advertise loading when profile policy blocks
|
||||
`skills_tool`.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
from agent import LoopData
|
||||
from helpers.extension import Extension
|
||||
from helpers import skills as skills_helper
|
||||
from helpers import skills as skills_helper, tool_policy
|
||||
|
||||
|
||||
class RecallRelevantSkills(Extension):
|
||||
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
|
||||
if not self.agent or loop_data.iteration != 0:
|
||||
return
|
||||
if not tool_policy.resolve_tool(self.agent, "skills_tool").allowed:
|
||||
return
|
||||
|
||||
content = loop_data.user_message.content if loop_data.user_message else ""
|
||||
if isinstance(content, dict):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
- Prompt additions must be bounded and compatible with tool-call contracts.
|
||||
- Discover local tool prompts through `helpers.subagents.get_paths` and apply
|
||||
`helpers.tool_policy` before including their text.
|
||||
- Omit the discoverable-skills catalog when profile policy blocks
|
||||
`skills_tool`; loaded skill history remains independent.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
from helpers.extension import Extension, extensible
|
||||
from helpers import skills as skills_helper
|
||||
from helpers import skills as skills_helper, tool_policy
|
||||
from agent import Agent, LoopData
|
||||
|
||||
|
||||
|
|
@ -22,6 +22,9 @@ class SkillsPrompt(Extension):
|
|||
|
||||
@extensible
|
||||
async def build_prompt(agent: Agent) -> str:
|
||||
if not tool_policy.resolve_tool(agent, "skills_tool").allowed:
|
||||
return ""
|
||||
|
||||
available = skills_helper.list_skills(agent=agent)
|
||||
result: list[str] = []
|
||||
for skill in available:
|
||||
|
|
|
|||
|
|
@ -92,12 +92,20 @@ def get_tool_catalog(agent: Any) -> list[dict[str, Any]]:
|
|||
tool_id = canonical_mcp_id(qualified)
|
||||
if tool_id in seen:
|
||||
continue
|
||||
server_name, _, tool_name = qualified.partition(".")
|
||||
seen.add(tool_id)
|
||||
catalog.append(
|
||||
{
|
||||
"id": tool_id,
|
||||
"name": qualified,
|
||||
"label": str(tool.get("name") or qualified),
|
||||
"label": " · ".join(
|
||||
part.replace("_", " ").strip().title()
|
||||
for part in (
|
||||
server_name,
|
||||
str(tool.get("title") or tool.get("name") or tool_name),
|
||||
)
|
||||
if part
|
||||
),
|
||||
"description": str(tool.get("description") or ""),
|
||||
"origin": f"MCP · {str(tool.get('server') or '').strip()}",
|
||||
"available": True,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@
|
|||
- Catalog descriptions call the supplied agent's prompt loader instead of
|
||||
opening prompt files through a parallel path; the editor agent intentionally
|
||||
keeps its existing raw, no-processor implementation.
|
||||
- MCP catalog labels include a human-readable server and tool name while
|
||||
canonical IDs retain the exact transport-qualified spelling.
|
||||
- Unknown policy IDs remain in the catalog as unavailable entries.
|
||||
- Resolution performs no model calls and logs no secrets.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
- `helpers/knowledge_import.py` and `helpers/memory_consolidation.py` own import and consolidation behavior.
|
||||
- `tools/` owns memory save/load/delete/forget and behavior adjustment tools.
|
||||
- `api/` and `webui/` own memory dashboard and knowledge reindex/import flows.
|
||||
- `prompts/`, `default_config.yaml`, and `plugin.yaml` own memory prompts, defaults, and metadata.
|
||||
- `prompts/`, `default_config.yaml`, and `plugin.yaml` own memory and behavior-tool prompts, defaults, and metadata.
|
||||
|
||||
## Local Contracts
|
||||
|
||||
|
|
|
|||
|
|
@ -229,7 +229,9 @@ def _load_loaded_skills_extension(monkeypatch, skill_root: Path):
|
|||
return importlib.import_module(module_name)
|
||||
|
||||
|
||||
def _load_relevant_skills_extension(monkeypatch, queries: list[str]):
|
||||
def _load_relevant_skills_extension(
|
||||
monkeypatch, queries: list[str], *, skills_tool_allowed: bool = True
|
||||
):
|
||||
extension_stub = types.ModuleType("helpers.extension")
|
||||
extension_stub.Extension = _FakeExtension
|
||||
monkeypatch.setitem(sys.modules, "helpers.extension", extension_stub)
|
||||
|
|
@ -247,9 +249,16 @@ def _load_relevant_skills_extension(monkeypatch, queries: list[str]):
|
|||
skills_stub.search_skills = _search_skills
|
||||
monkeypatch.setitem(sys.modules, "helpers.skills", skills_stub)
|
||||
|
||||
tool_policy_stub = types.ModuleType("helpers.tool_policy")
|
||||
tool_policy_stub.resolve_tool = lambda *args, **kwargs: types.SimpleNamespace(
|
||||
allowed=skills_tool_allowed
|
||||
)
|
||||
monkeypatch.setitem(sys.modules, "helpers.tool_policy", tool_policy_stub)
|
||||
|
||||
import helpers
|
||||
|
||||
monkeypatch.setattr(helpers, "skills", skills_stub, raising=False)
|
||||
monkeypatch.setattr(helpers, "tool_policy", tool_policy_stub, raising=False)
|
||||
|
||||
module_name = "extensions.python.message_loop_prompts_after._63_recall_relevant_skills"
|
||||
sys.modules.pop(module_name, None)
|
||||
|
|
@ -556,6 +565,29 @@ def test_relevant_skill_recall_uses_raw_user_message(monkeypatch):
|
|||
assert queries == ["Open a browser and take a screenshot."]
|
||||
|
||||
|
||||
def test_relevant_skill_recall_skips_blocked_skills_tool(monkeypatch):
|
||||
queries: list[str] = []
|
||||
module = _load_relevant_skills_extension(
|
||||
monkeypatch, queries, skills_tool_allowed=False
|
||||
)
|
||||
loop_data = types.SimpleNamespace(
|
||||
iteration=0,
|
||||
user_message=types.SimpleNamespace(
|
||||
content="Open a browser and take a screenshot.",
|
||||
output_text=lambda: "Open a browser and take a screenshot.",
|
||||
),
|
||||
extras_temporary={},
|
||||
)
|
||||
|
||||
asyncio.run(
|
||||
module.RecallRelevantSkills(types.SimpleNamespace()).execute(
|
||||
loop_data=loop_data
|
||||
)
|
||||
)
|
||||
|
||||
assert queries == []
|
||||
|
||||
|
||||
def test_skills_tool_read_file_action_reads_inside_skill_dir(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
|
|
@ -712,9 +744,10 @@ def test_behaviour_adjustment_normalizes_duplicate_rules(monkeypatch):
|
|||
|
||||
|
||||
def test_behaviour_prompts_preserve_exact_rules_and_avoid_promptinclude():
|
||||
behaviour_prompt = Path("prompts/agent.system.tool.behaviour.md").read_text(
|
||||
encoding="utf-8"
|
||||
behaviour_prompt_path = Path(
|
||||
"plugins/_memory/prompts/agent.system.tool.behaviour.md"
|
||||
)
|
||||
behaviour_prompt = behaviour_prompt_path.read_text(encoding="utf-8")
|
||||
merge_prompt = Path("prompts/behaviour.merge.sys.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
|
@ -724,6 +757,7 @@ def test_behaviour_prompts_preserve_exact_rules_and_avoid_promptinclude():
|
|||
|
||||
assert "exact-response rules" in behaviour_prompt
|
||||
assert "preserve it verbatim" in behaviour_prompt
|
||||
assert not Path("prompts/agent.system.tool.behaviour.md").exists()
|
||||
assert "respond exactly with a phrase" in merge_prompt
|
||||
assert "use behaviour_adjustment, not promptinclude files" in promptinclude_prompt
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from types import SimpleNamespace
|
|||
|
||||
import pytest
|
||||
|
||||
from extensions.python.system_prompt import _11_tools_prompt
|
||||
from extensions.python.system_prompt import _11_tools_prompt, _13_skills_prompt
|
||||
from helpers import mcp_handler, responses_tools, tool_policy
|
||||
from helpers.errors import RepairableException
|
||||
from plugins._tool_access.extensions.python.tool_execute_before._10_enforce_tool_policy import (
|
||||
|
|
@ -260,6 +260,65 @@ def test_catalog_keeps_installed_remote_tools_without_live_connector(
|
|||
assert [item["name"] for item in catalog] == ["code_execution_remote", "shell"]
|
||||
|
||||
|
||||
def test_mcp_catalog_labels_include_humanized_server_and_tool(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
class MCPTools:
|
||||
def get_tools(self):
|
||||
return [
|
||||
{
|
||||
"deep_wiki.ask_question": {
|
||||
"name": "ask_question",
|
||||
"description": "Ask DeepWiki",
|
||||
"server": "deep_wiki",
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(tool_policy.subagents, "get_paths", lambda *args, **kwargs: [])
|
||||
monkeypatch.setattr(mcp_handler.MCPConfig, "get_for_agent", lambda agent: MCPTools())
|
||||
monkeypatch.setattr(
|
||||
tool_policy,
|
||||
"get_policy",
|
||||
lambda agent: {
|
||||
"mode": "inherit",
|
||||
"default": "allow",
|
||||
"allowed": [],
|
||||
"blocked": [],
|
||||
},
|
||||
)
|
||||
|
||||
assert tool_policy.get_tool_catalog(_Agent(tmp_path)) == [
|
||||
{
|
||||
"id": "mcp:deep_wiki:ask_question",
|
||||
"name": "deep_wiki.ask_question",
|
||||
"label": "Deep Wiki · Ask Question",
|
||||
"description": "Ask DeepWiki",
|
||||
"origin": "MCP · deep_wiki",
|
||||
"available": True,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skills_catalog_prompt_is_absent_when_skills_tool_is_blocked(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(tool_policy.subagents, "get_paths", lambda *args, **kwargs: [])
|
||||
monkeypatch.setattr(
|
||||
tool_policy,
|
||||
"get_policy",
|
||||
lambda agent: _custom_policy(default="allow", blocked=["local:skills_tool"]),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
_13_skills_prompt.skills_helper,
|
||||
"list_skills",
|
||||
lambda **kwargs: pytest.fail("blocked skill discovery ran"),
|
||||
)
|
||||
|
||||
assert await _13_skills_prompt.build_prompt(_Agent(tmp_path)) == ""
|
||||
|
||||
|
||||
def test_tool_prompt_description_skips_fenced_examples() -> None:
|
||||
prompt = """### example
|
||||
~~~json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue