Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions agent/agent_runtime_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,11 @@ def _is_codex_interim(m: Dict) -> bool:
prev["tool_calls"] = prev_calls + new_calls
elif prev_calls:
prev["tool_calls"] = prev_calls
else:
# Both messages lack tool_calls — remove any stale empty array
# that would otherwise be preserved and later rejected by strict
# providers (DeepSeek: HTTP 400 "tool_calls: empty array").
prev.pop("tool_calls", None)
# Concatenate plain-text content; leave multimodal (list)
# content on either side alone to avoid mangling attachment
# blocks — fall back to keeping the existing content.
Expand Down
76 changes: 64 additions & 12 deletions hermes_cli/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,7 +737,7 @@ def _check_gateway_running(profile_dir: Path) -> bool:
# renders "全部智能体 0". We cache the count keyed by the skills dir, invalidated
# when the dir tree's signature (skills_dir + immediate category dirs mtimes)
# changes (catches skill add/remove) or after a short TTL (catches deep edits).
_SKILL_COUNT_CACHE: dict[str, tuple[float, float, int]] = {}
_SKILL_COUNT_CACHE: dict[str, tuple[tuple[float, ...], float, int]] = {}
_SKILL_COUNT_TTL_SECONDS = 30.0


Expand Down Expand Up @@ -768,29 +768,81 @@ def _skills_dir_signature(skills_dir: Path) -> float:
return sig


def _collect_skills_dirs(profile_dir: Path) -> List[Path]:
"""Return all skill directories visible to *profile_dir*.

Order: profile-specific skills first (own ``skills/``), then the global
``~/.hermes/skills/`` (via ``get_default_hermes_root`` so this is
unaffected by profile-scope overrides), then any
``skills.external_dirs`` from config.
"""
from agent.skill_utils import get_external_skills_dirs
from hermes_constants import get_default_hermes_root

dirs: List[Path] = []

profile_skills = profile_dir / "skills"
if profile_skills.is_dir():
dirs.append(profile_skills)

global_skills = get_default_hermes_root() / "skills"
if global_skills.is_dir() and global_skills not in dirs:
dirs.append(global_skills)

for ext_dir in get_external_skills_dirs():
if ext_dir not in dirs:
dirs.append(ext_dir)

return dirs


def _skills_dirs_signature(dirs: List[Path]) -> tuple[float, ...]:
"""Combined change-signature for multiple skill directories."""
return tuple(_skills_dir_signature(d) for d in dirs)


def _count_skills(profile_dir: Path) -> int:
"""Count installed skills in a profile (cached by skills-dir signature)."""
skills_dir = profile_dir / "skills"
if not skills_dir.is_dir():
"""Count total skills available to *profile_dir* (profile-specific +
global + external dirs, cached by combined dir signature).

Deduplicates by skill name (from YAML frontmatter) across directories so
a skill that appears in both the global dir and the profile's own dir is
counted once — matching how ``scan_skill_commands`` loads skills.
"""
dirs = _collect_skills_dirs(profile_dir)
if not dirs:
return 0

key = str(skills_dir)
signature = _skills_dir_signature(skills_dir)
key = ";".join(str(d) for d in dirs)
signatures = _skills_dirs_signature(dirs)
now = time.time()
cached = _SKILL_COUNT_CACHE.get(key)
if (
cached is not None
and cached[0] == signature
and cached[0] == signatures
and (now - cached[1]) < _SKILL_COUNT_TTL_SECONDS
):
return cached[2]

from agent.skill_utils import parse_frontmatter

count = 0
for md in skills_dir.rglob("SKILL.md"):
if is_excluded_skill_path(md):
continue
count += 1
_SKILL_COUNT_CACHE[key] = (signature, now, count)
seen_names: set = set()
for sdir in dirs:
for md in sdir.rglob("SKILL.md"):
if is_excluded_skill_path(md):
continue
# Dedup by skill name from frontmatter
try:
frontmatter, _ = parse_frontmatter(md.read_text(encoding="utf-8"))
name = frontmatter.get("name", md.parent.name)
except Exception:
name = md.parent.name
if name in seen_names:
continue
seen_names.add(name)
count += 1
_SKILL_COUNT_CACHE[key] = (signatures, now, count)
return count


Expand Down
2 changes: 1 addition & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6072,7 +6072,7 @@ def _sanitize_tool_calls_for_strict_api(api_msg: dict, model: "str | None" = Non
Fields stripped: call_id, response_item_id, extra_content (model-gated)
"""
tool_calls = api_msg.get("tool_calls")
if not isinstance(tool_calls, list):
if not isinstance(tool_calls, list) or not tool_calls:
return api_msg
from agent.transports.chat_completions import _model_consumes_thought_signature
_STRIP_KEYS = {"call_id", "response_item_id"}
Expand Down
Loading