From 54ccc1695677d0aebf9e1343ffc9e3927dcd147e Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jun 2026 15:17:37 +0300 Subject: [PATCH 01/19] skill md tool --- anton/core/memory/skill_md.py | 199 ++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 anton/core/memory/skill_md.py diff --git a/anton/core/memory/skill_md.py b/anton/core/memory/skill_md.py new file mode 100644 index 00000000..6fb0b088 --- /dev/null +++ b/anton/core/memory/skill_md.py @@ -0,0 +1,199 @@ +"""Parsing and serialisation of the agentskills.io SKILL.md format. + +SKILL.md layout: + --- + name: my-cat + description: Short description (1-1024 chars) + license: MIT # optional + compatibility: requires network # optional + allowed-tools: read_file write_file # optional, space-separated + metadata: + display_name: My Cat + when_to_use: ... + provenance: manual + created_at: "2026-06-15T15:20:42+00:00" + --- + Step-by-step body (= former declarative.md content) + +Parsing is intentionally lenient — no field validation is applied when +reading, since files may be authored by external tools. Validators on +SkillFrontmatter exist for write/creation paths only and must be called +explicitly via SkillFrontmatter.model_validate(). + +Unknown top-level YAML keys are folded into `metadata` automatically. +""" + +from __future__ import annotations + +import logging +import re +from typing import Any + +import yaml +from pydantic import BaseModel, Field, field_validator + +logger = logging.getLogger(__name__) + +# ─── spec constraints (used by validators, not enforced on read) ────────────── + +_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$") +_NAME_MAX = 64 +_DESC_MAX = 1024 +_COMPAT_MAX = 500 + +# canonical YAML keys defined by the spec +_SPEC_KEYS = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"} + + +# ─── model ──────────────────────────────────────────────────────────────────── + + +class SkillFrontmatter(BaseModel): + """In-memory representation of a SKILL.md frontmatter block. + + Validators are active when you call model_validate() (write / creation + path). parse_skill_md() uses model_construct() so validators are skipped. + """ + + name: str = "" + description: str = "" + license: str | None = None + compatibility: str | None = None + allowed_tools: str | None = Field(None, alias="allowed-tools") + metadata: dict[str, str] = {} + + # ── validators (write / creation path only) ─────────────────────── + + @field_validator("name") + @classmethod + def _validate_name(cls, v: str) -> str: + if len(v) > _NAME_MAX: + raise ValueError(f"name exceeds {_NAME_MAX} chars") + if "--" in v: + raise ValueError("name must not contain '--'") + if not _NAME_RE.match(v): + raise ValueError( + f"name {v!r} must match {_NAME_RE.pattern}" + ) + return v + + @field_validator("description") + @classmethod + def _validate_description(cls, v: str) -> str: + if not v: + raise ValueError("description must not be empty") + if len(v) > _DESC_MAX: + raise ValueError(f"description exceeds {_DESC_MAX} chars") + return v + + @field_validator("compatibility") + @classmethod + def _validate_compatibility(cls, v: str | None) -> str | None: + if v is not None and len(v) > _COMPAT_MAX: + raise ValueError(f"compatibility exceeds {_COMPAT_MAX} chars") + return v + + @field_validator("metadata", mode="before") + @classmethod + def _coerce_metadata(cls, v: Any) -> dict[str, str]: + if not isinstance(v, dict): + return {} + return {str(k): str(val) for k, val in v.items()} + + +# ─── parse ──────────────────────────────────────────────────────────────────── + + +def parse_skill_md(text: str, folder_name: str = "") -> tuple[SkillFrontmatter | None, str]: + """Parse SKILL.md text into (frontmatter, body). + + No field validation is applied — accepts files created by any tool. + Unknown top-level YAML keys are merged into metadata. + Returns (None, full_text) only on structural failures (missing delimiters, + broken YAML, non-mapping root). + """ + lines = text.split("\n") + if not lines or lines[0].rstrip() != "---": + logger.debug("parse_skill_md: no opening '---' delimiter") + return None, text + + close_idx: int | None = None + for i, line in enumerate(lines[1:], 1): + if line.rstrip() == "---": + close_idx = i + break + + if close_idx is None: + logger.debug("parse_skill_md: no closing '---' delimiter") + return None, text + + yaml_text = "\n".join(lines[1:close_idx]) + body = "\n".join(lines[close_idx + 1 :]) + + try: + props = yaml.safe_load(yaml_text) + except yaml.YAMLError as exc: + logger.warning("parse_skill_md: YAML error: %s", exc) + return None, text + + if not isinstance(props, dict): + logger.warning("parse_skill_md: frontmatter is not a YAML mapping") + return None, text + + # Collect metadata from the spec field, then fold in unknown top-level keys + meta: dict[str, str] = {} + spec_meta = props.get("metadata") + if isinstance(spec_meta, dict): + meta = {str(k): str(v) for k, v in spec_meta.items()} + for k, v in props.items(): + if k not in _SPEC_KEYS: + meta.setdefault(str(k), str(v)) + + # check name + name = props.get("name") + if not name: + name = folder_name + + name = re.sub(_NAME_RE, "", name).replace("--", " - ")[:_NAME_MAX] + if name.startswith("-"): + name = name[1:] + + fm = SkillFrontmatter.model_construct( + name=name, + description=str(props.get("description", ""))[:_DESC_MAX], + license=props.get("license"), + compatibility=props.get("compatibility"), + allowed_tools=props.get("allowed-tools"), + metadata=meta, + ) + return fm, body + + +# ─── dump ───────────────────────────────────────────────────────────────────── + + +def dump_skill_md(frontmatter: SkillFrontmatter, body: str) -> str: + """Serialise frontmatter + body back to SKILL.md text.""" + data: dict[str, Any] = { + "name": frontmatter.name, + "description": frontmatter.description, + } + if frontmatter.license is not None: + data["license"] = frontmatter.license + if frontmatter.compatibility is not None: + data["compatibility"] = frontmatter.compatibility + if frontmatter.allowed_tools is not None: + data["allowed-tools"] = frontmatter.allowed_tools + if frontmatter.metadata: + data["metadata"] = dict(frontmatter.metadata) + + yaml_text = yaml.dump( + data, + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + ) + return f"---\n{yaml_text}---\n{body}" + + +__all__ = ["SkillFrontmatter", "parse_skill_md", "dump_skill_md"] diff --git a/uv.lock b/uv.lock index 002d1055..08c2abc4 100644 --- a/uv.lock +++ b/uv.lock @@ -164,7 +164,7 @@ wheels = [ ] [[package]] -name = "anton" +name = "anton-agent" source = { editable = "." } dependencies = [ { name = "aiohttp" }, From 780f7d138804a01b1e00c9b19851c41c8a8da091 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 16 Jun 2026 16:53:04 +0300 Subject: [PATCH 02/19] update SkillStore --- anton/core/memory/skills.py | 377 +++++++++++++++++++++--------------- 1 file changed, 219 insertions(+), 158 deletions(-) diff --git a/anton/core/memory/skills.py b/anton/core/memory/skills.py index 0d0b1a87..1ea3ac59 100644 --- a/anton/core/memory/skills.py +++ b/anton/core/memory/skills.py @@ -2,67 +2,38 @@ A *skill* is one concept with multiple representations that coexist: - - Stage 1 (declarative.md) — step-by-step procedure the LLM reads. Always present. - - Stage 2 (chunks.md) — higher-level recipes/macros. Emerges from use. (v2+) - - Stage 3 (code/) — runnable helper modules. Emerges from reliability. (v2+) + - Stage 1 (SKILL.md body) — step-by-step procedure the LLM reads. Always present. + - Stage 2 (references/) — higher-level recipes/macros. Emerges from use. + - Stage 3 (scripts/) — runnable helper modules. Emerges from reliability. Each skill lives at `~/.anton/skills/