{"record":{"id":"9cfdffa59906dd0c","repo":"deepset-ai/haystack","slug":"hook-of-type-type-h-name-is-registered-un","errorCode":null,"errorMessage":"Hook of type '{type(h).__name__}' is registered under hook point '{hook_point}' but only supports: {', '.join(allowed_points)}.","messagePattern":"Hook of type '(.+?)' is registered under hook point '(.+?)' but only supports: (.+?)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/components/agents/agent.py","lineNumber":141,"sourceCode":"            raise ValueError(\n                f\"Invalid hook point '{hook_point}'. Valid hook points are: {', '.join(VALID_HOOK_POINTS)}.\"\n            )\n        for h in hook_list:\n            if not callable(getattr(h, \"run\", None)):\n                if callable(h):\n                    raise TypeError(\n                        f\"Hook registered for hook point '{hook_point}' is callable but is not a Hook object. \"\n                        \"If it is a function, wrap it with the @hook decorator.\"\n                    )\n                raise TypeError(\n                    f\"Hook registered for hook point '{hook_point}' must have a callable 'run(state)', \"\n                    f\"got an object of type '{type(h).__name__}'.\"\n                )\n            # A hook may declare `allowed_hook_points` to restrict where it can run (e.g. ConfirmationHook only\n            # makes sense at \"before_tool\"). Hooks without it can be registered under any hook point.\n            allowed_points = getattr(h, \"allowed_hook_points\", None)\n            if allowed_points is not None and hook_point not in allowed_points:\n                raise ValueError(\n                    f\"Hook of type '{type(h).__name__}' is registered under hook point '{hook_point}' but only \"\n                    f\"supports: {', '.join(allowed_points)}.\"\n                )\n\n\ndef _consume_continue_run(state: State) -> bool:\n    \"\"\"Return the `continue_run` control flag and reset it so it does not carry over to the next exit attempt.\"\"\"\n    should_continue = state.data[\"continue_run\"]\n    state.set(\"continue_run\", False)\n    return should_continue\n\n\ndef _get_model_exit_reason(messages: list[ChatMessage]) -> str | None:\n    \"\"\"\n    Return the exit reason for a terminal assistant reply without tool calls.\n\n    Incomplete generation reasons take precedence over text so callers can distinguish a partial response from a\n    complete answer. An empty response without a recognized terminal reason does not trigger an exit, preserving the","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/components/agents/agent.py#L123-L159","documentation":"During warm_up, every skill .md file's frontmatter must contain a non-empty 'description'. This ValueError is raised when frontmatter.get(\"description\") is missing, None, or an empty string. The description is required because it is surfaced to the model for skill selection.","triggerScenarios":"A skill markdown file in skills_dir whose YAML frontmatter lacks a 'description' key, has description: (empty/null), or description: \"\" ; triggered by warm_up via list_skills/load_skill or store construction-time warming.","commonSituations":"Authoring a new SKILL.md and forgetting the description field; stripping frontmatter keys with a linter/formatter; copying a skill template with placeholder removed; renaming keys (e.g. 'summary') instead of 'description'.","solutions":["Add a non-empty 'description: ...' line to the skill file's frontmatter.","If the file isn't meant to be a skill, move it out of skills_dir or into a subdirectory the scanner doesn't treat as a skill .md.","Check the exact file named in the error message and confirm the key spelling is 'description' (lowercase)."],"exampleFix":"// before (SKILL.md)\n---\nname: my-skill\n---\n// after\n---\nname: my-skill\ndescription: Explains how to use the my-skill workflow.\n---","handlingStrategy":"validation","validationCode":"import yaml\nfrom pathlib import Path\n\ndef skill_has_description(skill_file: Path) -> bool:\n    lines = skill_file.read_text(encoding=\"utf-8\").splitlines()\n    closing = lines.index(\"---\", 1)\n    fm = yaml.safe_load(\"\\n\".join(lines[1:closing])) or {}\n    return isinstance(fm, dict) and bool(fm.get(\"description\"))","typeGuard":"def has_nonempty_description(frontmatter: dict) -> bool:\n    return bool(frontmatter.get(\"description\"))","tryCatchPattern":"try:\n    store.list_skills()\nexcept ValueError as e:\n    if \"missing a 'description'\" in str(e):\n        logger.error(\"Add a description to the frontmatter of: %s\", e)\n    raise","preventionTips":["Start every new skill from a template that includes name and description","Add a CI test asserting every skills/**/*.md frontmatter has a non-empty description","Never remove or rename the 'description' key when editing skills"],"tags":["skill-store","frontmatter","validation","configuration"],"backgroundTag":"missing-required-field","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}