Skip to content

symfonic.core.contracts.skill_metadata

skill_metadata

How a procedural skill's tool-binding metadata is read (TA2.3).

Three layers have to agree on the shape of a procedural skill's metadata dict, and each of them reads it:

  • symfonic.memory.layers.procedural.layer (capability) writes the shape -- precondition, action_tool, steps -- and projects it back out of the graph store.
  • symfonic.memory.layers.procedural.router (capability) renders it into the router prompt's PRE-FLIGHT markers.
  • symfonic.core.nodes.precondition_gate, symfonic.agent.preflight_l1 and symfonic.agent.engine (facade-compiler) enforce and synthesise from it.

That is a vocabulary two layers must share, so under LAY-ADR it lives in the one column every row of the dependency matrix reads yes on: kernel-contracts. The readers below were the two the capability side reached for, and they used to live in core/nodes/precondition_gate.py -- a 842-line facade-compiler graph node the memory router may not import. precondition_gate re-exports them under their historical private names, so every existing import path still resolves to these definitions.

The module is stdlib-only, which is what the kernel-contracts purity rule requires; it holds the readers, not the gate. Enforcement -- the state predicate DSL, the history walk, the gate node itself -- stays in precondition_gate.

extract_bare_identifier

extract_bare_identifier(precondition: str) -> str | None

Return the tool name from a structured precondition string, or None if the string is free text.

The discriminator IS the bare-identifier regex. Adopters opt INTO enforcement by authoring preconditions as identifiers; free-text rules (LLM-extractor output, narrative prose) fall through.

Source code in src/symfonic/core/contracts/skill_metadata.py
def extract_bare_identifier(precondition: str) -> str | None:
    """Return the tool name from a structured precondition string, or
    ``None`` if the string is free text.

    The discriminator IS the bare-identifier regex.  Adopters opt INTO
    enforcement by authoring preconditions as identifiers; free-text
    rules (LLM-extractor output, narrative prose) fall through.
    """
    if not isinstance(precondition, str):
        return None
    stripped = precondition.strip()
    if not stripped:
        return None
    match = _BARE_IDENTIFIER_RE.match(stripped)
    if match is None:
        return None
    return match.group(1)

skill_action_tool

skill_action_tool(skill_entry: Any) -> str | None

Return the action tool name for a procedural skill, or None when no tool binding is discoverable.

Precedence:

  1. metadata['action_tool'] -- explicit binding adopters set on authored skills to disambiguate.
  2. metadata['steps'][0] when it matches the bare-identifier regex -- consistent with v7.6 draft_to_memory_entry which writes the action tool as the first step.
  3. None -- no enforceable binding; skill is skipped.
Source code in src/symfonic/core/contracts/skill_metadata.py
def skill_action_tool(skill_entry: Any) -> str | None:
    """Return the action tool name for a procedural skill, or ``None``
    when no tool binding is discoverable.

    Precedence:

    1. ``metadata['action_tool']`` -- explicit binding adopters set on
       authored skills to disambiguate.
    2. ``metadata['steps'][0]`` when it matches the bare-identifier
       regex -- consistent with v7.6 ``draft_to_memory_entry`` which
       writes the action tool as the first step.
    3. ``None`` -- no enforceable binding; skill is skipped.
    """
    md = getattr(skill_entry, "metadata", None) or {}
    if not isinstance(md, dict):
        # Defensive: tests + ill-formed skills sometimes set
        # ``metadata`` to a non-dict.  Degrade to "no action tool"
        # rather than raising AttributeError.
        return None
    explicit = md.get("action_tool")
    if isinstance(explicit, str):
        tool_name = extract_bare_identifier(explicit)
        if tool_name:
            return tool_name
    steps = md.get("steps")
    if isinstance(steps, list) and steps:
        first_step = steps[0]
        if isinstance(first_step, str):
            return extract_bare_identifier(first_step)
    return None