You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(memory): rename synthesis Skill dataclass to DistilledProcedure (#2684)
Two different frozen dataclasses were both named `Skill` and both
reachable from `gaia.agents.base`: the synthesis pipeline's four-field
LLM-output schema and the on-disk agentskills.io file contract landing
in `src/gaia/skills/` (#2669). The split is a deliberate security
boundary — the model may only derive
`name`/`when_to_use`/`body`/`tools_required`, never `license`,
`version`, or a security tier — so both classes stay; the synthesis one
becomes `DistilledProcedure`, matching its own docstring and the
`procedures` corpus it feeds. Anyone importing both no longer needs an
alias, and nobody can pass the wrong one.
Also adds the test `to_skill_md`'s docstring has been asserting all
along: the rendered document is now round-tripped through the real
`gaia.skills.format` parser + validator instead of a hand-rolled YAML
check. Verified against #2669's branch — **it validates, no drift**
(`tools_required` is a first-class `metadata.gaia` field there). The
test skips until #2669 merges.
Part of #2671.
## Test plan
- [ ] `python -m pytest tests/unit/test_skill_synthesis.py
tests/unit/test_memory_mixin.py tests/unit/test_chat_dynamic_tools.py
-q` — 310+ pass, the round-trip test skips while `gaia.skills` is
unmerged
- [ ] After #2669 merges, rebase and confirm
`test_renders_a_document_the_format_validator_accepts` runs and passes
rather than skipping
- [ ] `python -m pytest tests/unit/ -q` — no new failures vs.
`origin/main` (verified: identical failure set, all pre-existing
environment issues)
- [ ] `python util/lint.py --all` clean
- [ ] `grep -rn '\bSkill\b' src/gaia/agents/base/` returns only prose
section headers, no class references
|`name`|**LLM (derived)**|`name`| Identity, derived from the cluster. Matches the rule at [`skill-format.mdx:150`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx). |
255
255
|`when_to_use`|**LLM (derived)**|`description`|**`when_to_use` is the LLM-emitted field on purpose** — it forces the model to *conclude the trigger boundaries* of the skill; `description` is too vague a label to constrain it. Its embedding is what `recall_skill(goal)` matches against. Maps to the required `description` ([`skill-format.mdx:151`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx)). |
256
256
|`tools_required` (top-level) |**LLM (derived)**|`metadata.gaia.tools_required`| The tool set the procedure actually used. #691**locks**`tools_required` as the cross-spec recipe contract and nests it under `metadata.gaia` ([`skill-format.mdx:143`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx), `:158`). |
257
-
|`markdown_body`|**LLM (derived)**| body | The LLM writes the **full procedure — numbered steps *and* a `## Edge cases` section — inline**. `Skill.parse()` does not synthesize a separate edge-case section; the distilled body already carries it. |
258
-
|`license: MIT`, `version: 1.0.0`|**`Skill.parse()` (fixed)**|`license`, `version`|**Fixed constants, not learned content — the LLM does *not* emit them.**`license` is always the repository license ([`skill-format.mdx:152`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx)); a freshly synthesised skill is always `version: 1.0.0` (SemVer — [`skill-format.mdx:153`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx)). Injecting them keeps the prompt's output surface purely dynamic and removes any chance the model emits a wrong version. |
257
+
|`markdown_body`|**LLM (derived)**| body | The LLM writes the **full procedure — numbered steps *and* a `## Edge cases` section — inline**. `DistilledProcedure.parse()` does not synthesize a separate edge-case section; the distilled body already carries it. |
258
+
|`license: MIT`, `version: 1.0.0`|**`DistilledProcedure.parse()` (fixed)**|`license`, `version`|**Fixed constants, not learned content — the LLM does *not* emit them.**`license` is always the repository license ([`skill-format.mdx:152`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx)); a freshly synthesised skill is always `version: 1.0.0` (SemVer — [`skill-format.mdx:153`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx)). Injecting them keeps the prompt's output surface purely dynamic and removes any chance the model emits a wrong version. |
259
259
260
260
A synthesised recipe therefore emits a **bounded field set** — `name`,
261
261
`description`, `license`, `version`, `metadata.gaia.tools_required`, and the body.
@@ -269,10 +269,10 @@ most-restrictive `experimental` at load time.
269
269
This is the **one deliberate refinement** of the maintainer's pseudocode: his
270
270
`DISTILL_SYSTEM_PROMPT` listed `version: 1.0.0`*inside* the LLM's required output
this spec moves `version` (and `license`) to `Skill.parse()` injection because both
272
+
this spec moves `version` (and `license`) to `DistilledProcedure.parse()` injection because both
273
273
are fixed, not derived. The pipeline otherwise keeps the maintainer's prompt as
274
274
written, and **(a)** treats the four learned fields as the intermediate shape,
275
-
**(b)** specifies `Skill.parse()` as the translator to the locked schema, and
275
+
**(b)** specifies `DistilledProcedure.parse()` as the translator to the locked schema, and
276
276
**(c)** guarantees the emitted file validates under #691 — an explicit acceptance
277
277
criterion (see [Acceptance criteria traceability](#acceptance-criteria-traceability)).
278
278
@@ -448,7 +448,7 @@ current code; these are the corrections an implementer must internalize.
448
448
449
449
| Original framing | Reality on `main`| Consequence |
450
450
|---|---|---|
451
-
|`DISTILL_SYSTEM_PROMPT` emits `when_to_use` + top-level `tools_required` as the on-disk shape |#691 locks `description` + `metadata.gaia.tools_required` ([`skill-format.mdx:151`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx), `:158`) | The prompt fields are the **intermediate** shape; `Skill.parse()` maps them. See [The format contract](#the-format-contract). |
451
+
|`DISTILL_SYSTEM_PROMPT` emits `when_to_use` + top-level `tools_required` as the on-disk shape |#691 locks `description` + `metadata.gaia.tools_required` ([`skill-format.mdx:151`](https://github.com/amd/gaia/blob/main/docs/plans/skill-format.mdx), `:158`) | The prompt fields are the **intermediate** shape; `DistilledProcedure.parse()` maps them. See [The format contract](#the-format-contract). |
452
452
| Scope C: *"New **tool**: `recall_skill`"*| The memory registry is exactly five tools ([`memory.py:1984`](https://github.com/amd/gaia/blob/main/src/gaia/agents/base/memory.py)+) and the planner calls recall programmatically |`recall_skill` is an **internal method**, not a sixth `@tool`. See [Decided design](#decided-design). |
453
453
| Hook is `on_consolidation_pass` → `_consolidate_old_sessions / _reconcile_contradictions / _prune_stale`| Those names don't exist; the real pass is `_run_memory_post_init` → `reconcile_memory` + `consolidate_old_sessions` ([`memory.py:1206`](https://github.com/amd/gaia/blob/main/src/gaia/agents/base/memory.py)) | Bind `_synthesize_skills` to the **real** maintenance method. |
454
454
| Tunables in `~/.gaia/config.toml [memory.skill_synthesis]`| Memory is configured via `~/.gaia/memory_settings.json` ([`memory.py:57`](https://github.com/amd/gaia/blob/main/src/gaia/agents/base/memory.py)) + `GAIA_MEMORY_DISABLED`; there is **no**`config.toml``[memory]` section | Surface the thresholds on the **existing** memory-settings surface, not a new TOML section — see [Open questions](#open-questions). |
@@ -460,7 +460,7 @@ current code; these are the corrections an implementer must internalize.
460
460
-**The intermediate frontmatter is not the on-disk schema.** A reader of the
461
461
maintainer's `DISTILL_SYSTEM_PROMPT` must not write `when_to_use` /
462
462
top-level `tools_required` to disk. The on-disk file is the #691 schema
463
-
(`description`, `metadata.gaia.tools_required`); `Skill.parse()` is the only
463
+
(`description`, `metadata.gaia.tools_required`); `DistilledProcedure.parse()` is the only
464
464
bridge.
465
465
-**`recall_skill` is not an LLM tool.** Do not register a sixth memory `@tool`;
466
466
it would change the five-tool registry the rest of the system assumes.
@@ -510,7 +510,7 @@ mirroring the v1→v2 `ADD COLUMN` pattern
|**AC** — after 3 successful similar sequences, auto-create a `SKILL.md` discoverable via `recall_skill()`|[KPIs](#kpis), [Phase 1](#phased-build) + [Phase 2](#phased-build)| The headline KPI. |
781
781
|**AC** — recall measurably reduces tool-step count on the 4th attempt |[KPIs](#kpis), [Phase 2](#phased-build)| Measured vs the tool-loader baseline. |
782
-
|**AC** — synthesised skills are valid agentskills.io documents (Hermes parser) |[The format contract](#the-format-contract), [Examples A–B](#examples)|`Skill.parse()` round-trip; 100% validity KPI. |
782
+
|**AC** — synthesised skills are valid agentskills.io documents (Hermes parser) |[The format contract](#the-format-contract), [Examples A–B](#examples)|`DistilledProcedure.parse()` round-trip; 100% validity KPI. |
783
783
|**AC** — Dashboard shows count / last-used / provenance |[Phase 2 deferral](#phased-build)|**Data exposed by #887**; *rendering* is #606's tab. |
784
784
|**AC** — disabling a skill prevents recall |[Off-states](#off-states-as-safe-floors), [Phase 2](#phased-build)| The recall path honours `enabled = 0` (this **is**#887); the toggle UI is #606's tab. |
0 commit comments