mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 02:56:17 +00:00
* feat: emit structured runtime metadata * fix: avoid subagent import cycle in replay gateway * fix: preserve legacy subtask result parsing * refactor: tighten runtime metadata contracts * fix(middleware): keep recovery hint on task exception wrapper content The structured-metadata stamp overwrote the wrapper text with the bare task-failure message, dropping the model-facing 'Continue with available context, or choose an alternative tool.' guidance that every other tool exception keeps. Append the shared hint after the formatted message. * fix(subagents): require lowercase hex for result_sha256 reader Length-only validation accepted any 64-char string; a faulty serializer or relaying wrapper could store a non-digest value in the delegation ledger. Enforce the producer's hexdigest shape with a fullmatch. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
67 lines
2.0 KiB
Python
67 lines
2.0 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
from deerflow.constants import DEFAULT_SKILLS_CONTAINER_PATH
|
|
from deerflow.skills.types import Skill
|
|
|
|
RESERVED_SLASH_SKILL_NAMES = frozenset({"bootstrap", "goal", "help", "memory", "models", "new", "status"})
|
|
_SLASH_SKILL_RE = re.compile(r"^/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\s+|$)")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class SlashSkillReference:
|
|
"""Parsed slash-skill command with the skill name and remaining task text."""
|
|
|
|
name: str
|
|
remaining_text: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ResolvedSlashSkill:
|
|
"""Slash-skill activation resolved against enabled runtime-visible skills."""
|
|
|
|
skill: Skill
|
|
remaining_text: str
|
|
container_file_path: str
|
|
|
|
|
|
def parse_slash_skill_reference(text: str) -> SlashSkillReference | None:
|
|
"""Parse strict `/skill-name task` syntax, ignoring reserved control commands."""
|
|
match = _SLASH_SKILL_RE.match(text)
|
|
if not match:
|
|
return None
|
|
name = match.group(1)
|
|
if name in RESERVED_SLASH_SKILL_NAMES:
|
|
return None
|
|
return SlashSkillReference(
|
|
name=name,
|
|
remaining_text=text[match.end() :].lstrip(),
|
|
)
|
|
|
|
|
|
def resolve_slash_skill(
|
|
text: str,
|
|
skills: list[Skill],
|
|
*,
|
|
available_skills: set[str] | None = None,
|
|
container_base_path: str = DEFAULT_SKILLS_CONTAINER_PATH,
|
|
) -> ResolvedSlashSkill | None:
|
|
"""Resolve text into an enabled, whitelisted skill activation if possible."""
|
|
reference = parse_slash_skill_reference(text)
|
|
if reference is None:
|
|
return None
|
|
if available_skills is not None and reference.name not in available_skills:
|
|
return None
|
|
|
|
skill = next((candidate for candidate in skills if candidate.name == reference.name and candidate.enabled), None)
|
|
if skill is None:
|
|
return None
|
|
|
|
return ResolvedSlashSkill(
|
|
skill=skill,
|
|
remaining_text=reference.remaining_text,
|
|
container_file_path=skill.get_container_file_path(container_base_path),
|
|
)
|