mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-10-19 15:33:54 +00:00
Fix plugin precedence in subordinate profile settings
The profile-settings hook merged a priority-first lookup list with last-value-wins updates, allowing bundled settings to overwrite community plugins and higher user/project scopes. Merge plugin-wide settings, bundled profile, plugin profiles, user profile, project settings and project profile from low to high priority. Use the existing stable root-priority sort within each plugin layer, preserving same-root discovery order, activation scope and cached lookup lists. Add a failing-before/passing-after regression for both plugin layers, scoped overrides, fallback, malformed settings and profile/MCP inheritance. Strengthen metadata override tests with community plugins enabled and update the agent-init contract. Validation: 207 focused local tests and 18 precedence tests in the Docker framework runtime passed. Two isolated fresh-context initialization smoke checks passed in Docker. git diff --check passed.
This commit is contained in:
parent
a4e65d4721
commit
fe24597a6a
3 changed files with 100 additions and 2 deletions
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
- Keep initialization idempotent for contexts that may be restored or reloaded.
|
||||
- Preserve ordering between initial message creation and profile settings loading.
|
||||
- `_15_load_profile_settings.py` merges settings from low to high priority: plugin-wide files, bundled profile, plugin profiles, user profile, project-wide file, then project profile. Each plugin layer applies bundled roots before user roots with a stable root-priority sort, preserving same-root discovery order and cached lookup lists. Global framework and `usr/settings.json` files remain excluded; malformed layers log an error and leave other layers usable. Profile and MCP configuration inherit when not explicitly overridden.
|
||||
- Keep a placeholder user turn (`fw.initial_user_message.md`) ahead of the AI greeting (`fw.initial_message.md`) so `output_langchain` never pops the greeting as a leading `AIMessage`; do not remove either prompt without replacing the turn-order guarantee.
|
||||
- Minify the initial AI message JSON before storage; preserve raw text as the displayed greeting when JSON parsing fails.
|
||||
- `_10_initial_message.py` passes `LLMResult.non_llm()` to `hist_add_ai_response` so the Responses-API state seam runs uniformly; the sentinel carries no `response_id` and marks `mode=""`/`state="off"` so stored metadata does not claim a Responses-API turn.
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
## Verification
|
||||
|
||||
- Smoke-test new chat/context initialization after changes.
|
||||
- Run `tests/test_plugin_asset_precedence.py` and `tests/test_subagent_profiles.py` for profile-settings changes.
|
||||
|
||||
## Child DOX Index
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from initialize import initialize_agent
|
||||
from helpers import dirty_json, files, subagents, projects
|
||||
from helpers import dirty_json, files, subagents, plugins
|
||||
from helpers.extension import Extension
|
||||
|
||||
|
||||
|
|
@ -10,7 +10,25 @@ class LoadProfileSettings(Extension):
|
|||
if not self.agent or not self.agent.config.profile:
|
||||
return
|
||||
|
||||
config_files = subagents.get_paths(self.agent, "settings.json", include_default=False, include_user=False)
|
||||
profile = self.agent.config.profile
|
||||
scoped_files = subagents.get_paths(
|
||||
self.agent, "settings.json", include_default=False,
|
||||
include_user=False, include_plugins=False,
|
||||
)
|
||||
default_settings = files.get_abs_path(subagents.DEFAULT_AGENTS_DIR, profile, "settings.json")
|
||||
# Merge low-priority layers first, keeping discovery order within plugin roots.
|
||||
config_files = [
|
||||
*sorted(
|
||||
plugins.get_enabled_plugin_paths(self.agent, "settings.json"),
|
||||
key=plugins.get_plugin_root_priority, reverse=True,
|
||||
),
|
||||
default_settings,
|
||||
*sorted(
|
||||
plugins.get_enabled_plugin_paths(self.agent, "agents", profile, "settings.json"),
|
||||
key=plugins.get_plugin_root_priority, reverse=True,
|
||||
),
|
||||
*(path for path in reversed(scoped_files) if path != default_settings),
|
||||
]
|
||||
settings_override = {}
|
||||
for settings_path in config_files:
|
||||
if files.exists(settings_path):
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ def test_empty_asset_results_are_cached(plugin_tree, monkeypatch):
|
|||
monkeypatch.setattr(plugins, "get_enabled_plugins", unexpected_lookup)
|
||||
assert plugins.get_enabled_plugin_paths(None, "missing") == []
|
||||
|
||||
|
||||
def test_provider_merges_apply_user_roots_last_and_preserve_same_root_order(plugin_tree):
|
||||
roots = [
|
||||
plugin_tree("_alpha", bundled=True), plugin_tree("_zulu", bundled=True),
|
||||
|
|
@ -240,6 +241,7 @@ def test_profile_merges_preserve_root_order_and_higher_scope_overrides(
|
|||
state = editor.metadata_state("researcher", editor._EditorContext(project_name or ""))
|
||||
assert state["title"]["effective"] == state["context"]["effective"] == title
|
||||
else:
|
||||
# The global catalog includes profiles from every project.
|
||||
assert subagents.get_all_agents_list() == [{"key": "researcher", "label": title}]
|
||||
|
||||
for root in roots:
|
||||
|
|
@ -254,12 +256,88 @@ def test_profile_merges_preserve_root_order_and_higher_scope_overrides(
|
|||
cache.clear("*(plugins)*")
|
||||
assert_profile("_zulu")
|
||||
|
||||
for root in roots[2:]:
|
||||
(root / ".toggle-0").unlink()
|
||||
cache.clear("*(plugins)*")
|
||||
write_profile(tmp_path / "usr", "User profile")
|
||||
assert_profile("User profile")
|
||||
write_profile(tmp_path / "usr/projects/demo/.a0proj", "Project profile")
|
||||
assert_profile("Project profile", "demo")
|
||||
|
||||
|
||||
def test_profile_settings_merge_preserves_root_and_scope_priorities(
|
||||
plugin_tree, tmp_path, monkeypatch
|
||||
):
|
||||
from extensions.python.agent_init import _15_load_profile_settings as hook
|
||||
|
||||
roots = [
|
||||
plugin_tree("_alpha", bundled=True), plugin_tree("_zulu", bundled=True),
|
||||
plugin_tree("alpha"), plugin_tree("zulu"),
|
||||
]
|
||||
|
||||
def write_settings(root, **values):
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
path = root / "settings.json"
|
||||
path.write_text(json.dumps(values), encoding="utf-8")
|
||||
return path
|
||||
|
||||
for root in roots:
|
||||
write_settings(root, root_value=root.name, winner=root.name)
|
||||
write_settings(
|
||||
root / "agents/researcher", profile_value=root.name, winner=root.name
|
||||
)
|
||||
write_settings(tmp_path / "agents/researcher", default_only="inherited", winner="default")
|
||||
write_settings(tmp_path, excluded_default=True)
|
||||
write_settings(tmp_path / "usr", excluded_user=True)
|
||||
|
||||
received = []
|
||||
errors = []
|
||||
monkeypatch.setattr(
|
||||
hook, "initialize_agent",
|
||||
lambda override_settings: received.append(override_settings)
|
||||
or SimpleNamespace(profile="default", mcp_servers=""),
|
||||
)
|
||||
agent = SimpleNamespace(
|
||||
config=SimpleNamespace(profile="researcher", mcp_servers="inherited MCP"),
|
||||
context=SimpleNamespace(
|
||||
get_data=lambda *_args: "demo",
|
||||
log=SimpleNamespace(log=lambda **entry: errors.append(entry)),
|
||||
),
|
||||
)
|
||||
|
||||
def assert_settings(winner, plugin_winner="zulu"):
|
||||
hook.LoadProfileSettings(agent).execute()
|
||||
values = received[-1]
|
||||
assert values["winner"] == winner
|
||||
assert values["root_value"] == values["profile_value"] == plugin_winner
|
||||
assert values["default_only"] == "inherited"
|
||||
assert "excluded_default" not in values and "excluded_user" not in values
|
||||
assert agent.config.profile == "researcher"
|
||||
assert agent.config.mcp_servers == "inherited MCP"
|
||||
|
||||
assert_settings("zulu")
|
||||
assert plugins.get_enabled_plugin_paths(agent, "settings.json") == [
|
||||
str(root / "settings.json") for root in [*roots[2:], *roots[:2]]
|
||||
]
|
||||
write_settings(tmp_path / "usr/agents/researcher", winner="user")
|
||||
assert_settings("user")
|
||||
project = tmp_path / "usr/projects/demo/.a0proj"
|
||||
write_settings(project, winner="project")
|
||||
assert_settings("project")
|
||||
project_profile = write_settings(project / "agents/researcher", winner="")
|
||||
assert_settings("")
|
||||
|
||||
for root in roots[2:]:
|
||||
(root / ".toggle-0").touch()
|
||||
cache.clear("*(plugins)*")
|
||||
assert_settings("", "_zulu")
|
||||
|
||||
project_profile.write_text("[]", encoding="utf-8")
|
||||
assert_settings("project", "_zulu")
|
||||
assert len(errors) == 1 and errors[0]["type"] == "error"
|
||||
assert str(project_profile) in errors[0]["content"]
|
||||
|
||||
|
||||
def test_model_prompt_native_schema_and_dispatch_use_the_same_plugin(
|
||||
plugin_tree, monkeypatch, tmp_path
|
||||
):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue