mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-02 19:58:46 +00:00
_parse_model_experience stripped a bare string and returned it, so
`model_experience: " "` parsed as {"what_model_sees": ""} and every
model-visible surface rendered a "Model experience:" label with nothing after
it. The mapping form has always refused exactly that, with a fix_hint. Two
shapes of the same authoring mistake, one of them silent.
The fix is a subtraction: the string form IS the one-key mapping form, so it
now routes through the mapping branch instead of carrying its own return.
That is what makes the two refusals identical by construction rather than by
a copied message, and it removes a line rather than adding a branch.
Red-first: the two new parametrized cases (" " and "\n\t \n") DID NOT RAISE
before the change; green after, with test_skill_manifest_v11, test_contracts
and test_skill_loader.
214 lines
7.8 KiB
Python
214 lines
7.8 KiB
Python
"""CPL-7 Model Experience: manifest section + model-visible rendering + teaching refusals.
|
|
|
|
Pins (plan §7 item 7): a manifest WITH the section parses and reaches the
|
|
model-visible surfaces (summarize_skills → list_skills JSON, installed-skills
|
|
context section); a manifest WITHOUT it keeps the exact prior behavior; a
|
|
registration refusal explains how to fix the manifest (typed ``fix_hint``).
|
|
"""
|
|
|
|
import json
|
|
import pathlib
|
|
|
|
import pytest
|
|
|
|
from ouroboros.contracts.skill_manifest import (
|
|
MODEL_EXPERIENCE_KEYS,
|
|
SkillManifestError,
|
|
parse_skill_manifest_text,
|
|
)
|
|
|
|
REPO = pathlib.Path(__file__).resolve().parent.parent
|
|
|
|
|
|
# --- manifest schema -------------------------------------------------------
|
|
|
|
|
|
def test_manifest_parses_string_form_as_prose():
|
|
manifest = parse_skill_manifest_text(json.dumps({
|
|
"name": "x", "description": "d", "version": "1", "type": "instruction",
|
|
"model_experience": "adds a short cheat-sheet to context",
|
|
}))
|
|
assert manifest.model_experience == {"what_model_sees": "adds a short cheat-sheet to context"}
|
|
|
|
|
|
def test_manifest_parses_mapping_form_trimmed():
|
|
manifest = parse_skill_manifest_text(
|
|
"---\n"
|
|
"name: y\n"
|
|
"description: d\n"
|
|
"version: '1'\n"
|
|
"type: instruction\n"
|
|
"model_experience:\n"
|
|
" what_model_sees: ' prose here '\n"
|
|
" token_effect: 'small fixed cost'\n"
|
|
"---\nbody\n"
|
|
)
|
|
assert manifest.model_experience == {
|
|
"what_model_sees": "prose here",
|
|
"token_effect": "small fixed cost",
|
|
}
|
|
|
|
|
|
def test_manifest_without_section_is_none_and_not_extra():
|
|
manifest = parse_skill_manifest_text(json.dumps({
|
|
"name": "z", "description": "d", "version": "1", "type": "instruction",
|
|
}))
|
|
assert manifest.model_experience is None
|
|
assert "model_experience" not in manifest.raw_extra
|
|
|
|
|
|
@pytest.mark.parametrize("bad_section, expected_problem_bit", [
|
|
({"wat": "x"}, "unknown keys"),
|
|
({"what_model_sees": ["not", "prose"]}, "must be a prose string"),
|
|
({}, "at least one non-empty prose field"),
|
|
({"what_model_sees": " "}, "at least one non-empty prose field"),
|
|
# Same teaching error for the STRING form: a section the author filled in
|
|
# with nothing but whitespace was accepted and stored as empty prose, so
|
|
# every model-visible surface rendered a "Model experience:" label with no
|
|
# sentence after it. The mapping form has always refused it.
|
|
(" ", "at least one non-empty prose field"),
|
|
("\n\t \n", "at least one non-empty prose field"),
|
|
(42, "prose string or a mapping"),
|
|
])
|
|
def test_model_experience_refusals_teach(bad_section, expected_problem_bit):
|
|
with pytest.raises(SkillManifestError) as excinfo:
|
|
parse_skill_manifest_text(json.dumps({
|
|
"name": "w", "description": "d", "version": "1", "type": "instruction",
|
|
"model_experience": bad_section,
|
|
}))
|
|
err = excinfo.value
|
|
assert expected_problem_bit in err.problem
|
|
assert err.fix_hint # every refusal of the new section teaches the repair
|
|
assert err.fix_hint in str(err)
|
|
assert "fix:" in str(err)
|
|
|
|
|
|
def test_legacy_refusal_sites_teach_the_repair():
|
|
"""Representative pre-existing refusals now carry a non-empty fix_hint."""
|
|
cases = [
|
|
{"name": "a", "description": "d", "version": "1", "type": "script",
|
|
"scripts": "not-a-list"},
|
|
{"name": "b", "description": "d", "version": "1", "type": "instruction",
|
|
"schema_version": 99},
|
|
{"name": "c", "description": "d", "version": "1", "type": "extension",
|
|
"entry": "plugin.py", "plugin_api": {"version": "2.0", "surprise": True}},
|
|
]
|
|
for payload in cases:
|
|
with pytest.raises(SkillManifestError) as excinfo:
|
|
parse_skill_manifest_text(json.dumps(payload))
|
|
assert excinfo.value.fix_hint, f"refusal for {payload} does not teach"
|
|
assert excinfo.value.fix_hint in str(excinfo.value)
|
|
|
|
|
|
def test_model_experience_keys_are_the_contract():
|
|
assert MODEL_EXPERIENCE_KEYS == ("what_model_sees", "token_effect")
|
|
|
|
|
|
# --- model-visible surfaces ------------------------------------------------
|
|
|
|
|
|
def _install_skill(drive: pathlib.Path, name: str, *, model_experience: str = "") -> None:
|
|
from ouroboros.skill_loader import compute_content_hash
|
|
|
|
skill_dir = drive / "skills" / "external" / name
|
|
skill_dir.mkdir(parents=True)
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\n"
|
|
f"name: {name}\n"
|
|
"description: does a thing\n"
|
|
"version: 1.0.0\n"
|
|
"type: instruction\n"
|
|
"when_to_use: user asks for the thing\n"
|
|
+ model_experience
|
|
+ "---\nbody\n",
|
|
encoding="utf-8",
|
|
)
|
|
digest = compute_content_hash(skill_dir)
|
|
state = drive / "state" / "skills" / name
|
|
state.mkdir(parents=True)
|
|
(state / "enabled.json").write_text(json.dumps({"enabled": True}), encoding="utf-8")
|
|
(state / "review.json").write_text(
|
|
json.dumps({"status": "pass", "content_hash": digest}), encoding="utf-8"
|
|
)
|
|
|
|
|
|
_SECTION_YAML = (
|
|
"model_experience:\n"
|
|
" what_model_sees: one bounded cheat-sheet block\n"
|
|
" token_effect: about two hundred tokens per round\n"
|
|
)
|
|
|
|
|
|
def test_summarize_skills_carries_model_experience(tmp_path):
|
|
from ouroboros.skill_loader import summarize_skills
|
|
|
|
_install_skill(tmp_path, "with_exp", model_experience=_SECTION_YAML)
|
|
_install_skill(tmp_path, "without_exp")
|
|
rows = {row["name"]: row for row in summarize_skills(tmp_path)["skills"]}
|
|
assert rows["with_exp"]["model_experience"] == {
|
|
"what_model_sees": "one bounded cheat-sheet block",
|
|
"token_effect": "about two hundred tokens per round",
|
|
}
|
|
assert rows["without_exp"]["model_experience"] is None
|
|
|
|
|
|
def test_context_section_renders_model_experience(tmp_path):
|
|
from ouroboros.context import _build_installed_skills_section
|
|
|
|
repo = tmp_path / "repo"
|
|
drive = tmp_path / "data"
|
|
repo.mkdir()
|
|
drive.mkdir()
|
|
_install_skill(drive, "with_exp", model_experience=_SECTION_YAML)
|
|
|
|
class Env:
|
|
repo_dir = repo
|
|
drive_root = drive
|
|
|
|
section = _build_installed_skills_section(Env())
|
|
assert "with_exp" in section
|
|
assert "Model experience: one bounded cheat-sheet block" in section
|
|
assert "Token effect: about two hundred tokens per round" in section
|
|
|
|
|
|
def test_context_section_without_section_unchanged(tmp_path):
|
|
from ouroboros.context import _build_installed_skills_section
|
|
|
|
repo = tmp_path / "repo"
|
|
drive = tmp_path / "data"
|
|
repo.mkdir()
|
|
drive.mkdir()
|
|
_install_skill(drive, "plain")
|
|
|
|
class Env:
|
|
repo_dir = repo
|
|
drive_root = drive
|
|
|
|
section = _build_installed_skills_section(Env())
|
|
assert "plain" in section
|
|
assert "Model experience" not in section
|
|
assert "Token effect" not in section
|
|
|
|
|
|
def test_bundled_skills_declare_model_experience():
|
|
for rel in ("skills/telegram/SKILL.md", "skills/unix_computer_use/SKILL.md"):
|
|
manifest = parse_skill_manifest_text((REPO / rel).read_text(encoding="utf-8"))
|
|
section = manifest.model_experience
|
|
assert section, f"{rel} lacks the Model Experience section"
|
|
assert section.get("what_model_sees"), rel
|
|
assert section.get("token_effect"), rel
|
|
|
|
|
|
def test_adapter_frontmatter_preserves_model_experience():
|
|
from ouroboros.marketplace.adapter import _manifest_frontmatter_dict, _render_frontmatter
|
|
|
|
manifest = parse_skill_manifest_text(json.dumps({
|
|
"name": "hub_skill", "description": "d", "version": "1",
|
|
"type": "instruction",
|
|
"model_experience": {"what_model_sees": "prose survives adaptation"},
|
|
}))
|
|
front = _manifest_frontmatter_dict(manifest)
|
|
assert front["model_experience"] == {"what_model_sees": "prose survives adaptation"}
|
|
rendered = _render_frontmatter(front)
|
|
assert "model_experience" in rendered
|
|
assert "prose survives adaptation" in rendered
|