fix(skills): reuse the resolved app config in the no-arg skills prompt section (#4160)

get_skills_prompt_section() without app_config resolved get_app_config()
only to read container_path, then let the enabled-skills load fall back
to the warm cache. On a cold start the cache is empty and the first call
returns an empty skills list while the synchronously-loaded disabled
section is populated, so manually assembled agents (create_deerflow_agent
style integrations) got a prompt with no enabled skills.

Rebind the resolved config so the storage and enabled-skills loads below
use it too; when no config is resolvable the cache-only fallback is
unchanged. Adds a cold-cache regression test.

Fixes #4144

Co-authored-by: fancyboi999 <fancyboi999@users.noreply.github.com>
This commit is contained in:
Xinmin Zeng
2026-07-15 19:56:09 +08:00
committed by GitHub
co-authored by fancyboi999
parent e7e9c51988
commit 16919f7c52
2 changed files with 41 additions and 5 deletions
@@ -805,10 +805,16 @@ def get_skills_prompt_section(
try:
from deerflow.config import get_app_config
config = get_app_config()
container_base_path = config.skills.container_path
skill_evolution_enabled = config.skill_evolution.enabled
# Rebind so the storage/enabled-skills loads below use this resolved
# config too. Reading only container_path here and then letting
# get_enabled_skills_for_config(None) fall back to the warm cache
# rendered an empty enabled-skills list on a cold start while the
# synchronously-loaded disabled section was populated (#4144).
app_config = get_app_config()
container_base_path = app_config.skills.container_path
skill_evolution_enabled = app_config.skill_evolution.enabled
except Exception:
app_config = None
container_base_path = DEFAULT_SKILLS_CONTAINER_PATH
skill_evolution_enabled = False
else:
+32 -2
View File
@@ -11,7 +11,7 @@ class NamedTool:
self.name = name
def _make_skill(name: str, allowed_tools: list[str] | None = None) -> Skill:
def _make_skill(name: str, allowed_tools: list[str] | None = None, *, enabled: bool = True) -> Skill:
return Skill(
name=name,
description=f"Description for {name}",
@@ -21,7 +21,7 @@ def _make_skill(name: str, allowed_tools: list[str] | None = None) -> Skill:
relative_path=Path(name),
category="public",
allowed_tools=tuple(allowed_tools) if allowed_tools is not None else None,
enabled=True,
enabled=enabled,
)
@@ -80,6 +80,36 @@ def test_get_skills_prompt_section_returns_all_when_available_skills_is_none(mon
assert "skill2" in result
def test_get_skills_prompt_section_no_arg_cold_cache_loads_enabled_skills(monkeypatch):
"""#4144: a fresh process calling the no-arg helper must not render an empty
enabled-skills list while the synchronously-loaded disabled section is populated."""
import threading
from deerflow.agents.lead_agent import prompt as prompt_mod
skills = [_make_skill("skill1"), _make_skill("skill2", enabled=False)]
mock_storage = SimpleNamespace(load_skills=lambda *, enabled_only: [s for s in skills if s.enabled or not enabled_only])
monkeypatch.setattr("deerflow.agents.lead_agent.prompt.get_or_new_skill_storage", lambda **kwargs: mock_storage)
monkeypatch.setattr("deerflow.agents.lead_agent.prompt.get_or_new_user_skill_storage", lambda user_id, **kwargs: mock_storage)
monkeypatch.setattr(
"deerflow.config.get_app_config",
lambda: SimpleNamespace(
skills=SimpleNamespace(container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage", get_skills_path=lambda: Path("/tmp/skills")),
skill_evolution=SimpleNamespace(enabled=False),
),
)
# Cold cache: no warmed enabled-skills list, and the background refresh must
# not fill it mid-test — the reporter's cold start loses exactly this race.
monkeypatch.setattr(prompt_mod, "_enabled_skills_cache", None)
monkeypatch.setattr(prompt_mod, "_ensure_enabled_skills_cache", lambda: threading.Event())
result = get_skills_prompt_section(available_skills=None)
assert "<available_skills>" in result
assert "skill1" in result
assert "<disabled_skills>" in result
def test_get_skills_prompt_section_includes_slash_activation_guidance(monkeypatch):
skills = [_make_skill("data-analysis")]
monkeypatch.setattr("deerflow.agents.lead_agent.prompt._get_enabled_skills", lambda: skills)