UAT: Skill.from_config() silently drops tool reference override fields (description, writes, checkpointable) from YAML — schema and domain model mismatch #3923

Open
opened 2026-04-06 07:28:43 +00:00 by freemo · 0 comments
Owner

Metadata

  • Branch: fix/skill-from-config-tool-ref-override-fields
  • Commit Message: fix(skill): preserve tool reference override fields in Skill.from_config()
  • Milestone: (none — see backlog note below)
  • Parent Epic: #392

Backlog note: This issue was discovered during autonomous operation
on milestone v3.2.0. It does not block milestone completion and has been
placed in the backlog for human review and future milestone assignment.

Background and Context

The docs/schema/skill.schema.yaml defines tool references as rich objects with per-skill override fields (description, writes, checkpointable). The SkillConfigSchema in src/cleveragents/skills/schema.py correctly models these overrides via SkillToolRefSchema. However, the Skill domain model in src/cleveragents/domain/models/core/skill.py stores tool_refs as list[str] — plain strings — meaning Skill.from_config() silently drops all override fields when constructing the domain object from a YAML-sourced config dict.

This creates a silent data-loss path: YAML → SkillConfigSchema (overrides preserved) → Skill.from_config() (overrides dropped). Users who define tool reference overrides in their skill YAML (as documented in the schema) will have those overrides silently ignored with no error raised.

Current Behavior

Skill.from_config() in src/cleveragents/domain/models/core/skill.py line 340:

tool_refs=config.get("tools", config.get("tool_refs", [])),

The tool_refs field in the Skill domain model is list[str]. When from_config() receives a list of dicts (the YAML schema format), it passes the raw list of dicts directly to tool_refs. Pydantic coerces each dict to a string, silently dropping the override fields.

from cleveragents.domain.models.core.skill import Skill

config = {
    "name": "local/my-skill",
    "description": "Test skill",
    "tools": [
        {
            "name": "builtin/shell_execute",
            "description": "Execute commands in sandbox",  # Override
            "writes": True,                                # Override
            "checkpointable": False                        # Override
        }
    ]
}

skill = Skill.from_config(config)
# skill.tool_refs == ["builtin/shell_execute"] — overrides silently dropped
# No error is raised

Expected Behavior

Per docs/schema/skill.schema.yaml, tool references are objects with override fields:

tools:
  type: array
  items:
    type: object
    required_fields: [name]
    fields:
      name:
        type: string
        required: true
      description:
        type: string
        required: false
        description: "Override the tool's registered description within this skill context."
      writes:
        type: boolean
        required: false
        description: "Override the tool's writes capability flag."
      checkpointable:
        type: boolean
        required: false
        description: "Override the tool's checkpointable capability flag."

Skill.from_config() should parse each tool reference dict into a structured SkillToolRef domain object that preserves all override fields. The SkillResolver should then apply these overrides when assembling the skill's tool set.

Code Locations

  • src/cleveragents/domain/models/core/skill.py line 239 — tool_refs: list[str] (no override support)
  • src/cleveragents/domain/models/core/skill.py line 340 — from_config() passes raw list to tool_refs
  • docs/schema/skill.schema.yaml — documents description, writes, checkpointable override fields on tool refs
  • src/cleveragents/skills/schema.py SkillToolRefSchema — correctly models override fields (reference for correct behaviour)

Subtasks

  • Add a SkillToolRef dataclass or Pydantic model to src/cleveragents/domain/models/core/skill.py with fields: name: str, description: str | None, writes: bool | None, checkpointable: bool | None
  • Change tool_refs: list[str] to tool_refs: list[SkillToolRef] in the Skill domain model
  • Update Skill.from_config() to parse each tool reference entry (whether a plain string or a dict) into a SkillToolRef object, preserving all override fields
  • Update SkillResolver (and any other consumers of skill.tool_refs) to read override fields from SkillToolRef objects when populating per-tool overrides
  • Write Behave test scenarios verifying that description, writes, and checkpointable override fields are preserved end-to-end through from_config()
  • Write Behave test scenario verifying backward compatibility: plain string tool refs (no overrides) still parse correctly
  • Write negative test: verify that a tool ref dict missing the required name field raises a validation error
  • Run nox -e lint and nox -e typecheck to verify no regressions
  • Run nox -e unit_tests and nox -e coverage_report to confirm coverage >= 97%

Definition of Done

  • Skill.tool_refs stores SkillToolRef objects that carry all override fields defined in docs/schema/skill.schema.yaml
  • Skill.from_config() correctly parses both plain-string and dict-format tool references from YAML-sourced config
  • Override fields (description, writes, checkpointable) are preserved and accessible on the Skill domain object after from_config()
  • SkillResolver correctly applies per-tool overrides sourced from SkillToolRef objects
  • The data path YAML → SkillConfigSchemaSkill.from_config()SkillResolver is lossless for tool reference override fields
  • Behave tests cover positive cases (overrides preserved), backward-compat cases (plain strings), and negative cases (missing name)
  • docs/schema/skill.schema.yaml and src/cleveragents/domain/models/core/skill.py are fully consistent with each other
  • All nox stages pass
  • Coverage >= 97%

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `fix/skill-from-config-tool-ref-override-fields` - **Commit Message**: `fix(skill): preserve tool reference override fields in Skill.from_config()` - **Milestone**: *(none — see backlog note below)* - **Parent Epic**: #392 > **Backlog note:** This issue was discovered during autonomous operation > on milestone v3.2.0. It does not block milestone completion and has been > placed in the backlog for human review and future milestone assignment. ## Background and Context The `docs/schema/skill.schema.yaml` defines tool references as rich objects with per-skill override fields (`description`, `writes`, `checkpointable`). The `SkillConfigSchema` in `src/cleveragents/skills/schema.py` correctly models these overrides via `SkillToolRefSchema`. However, the `Skill` domain model in `src/cleveragents/domain/models/core/skill.py` stores `tool_refs` as `list[str]` — plain strings — meaning `Skill.from_config()` silently drops all override fields when constructing the domain object from a YAML-sourced config dict. This creates a silent data-loss path: YAML → `SkillConfigSchema` (overrides preserved) → `Skill.from_config()` (overrides dropped). Users who define tool reference overrides in their skill YAML (as documented in the schema) will have those overrides silently ignored with no error raised. ## Current Behavior `Skill.from_config()` in `src/cleveragents/domain/models/core/skill.py` line 340: ```python tool_refs=config.get("tools", config.get("tool_refs", [])), ``` The `tool_refs` field in the `Skill` domain model is `list[str]`. When `from_config()` receives a list of dicts (the YAML schema format), it passes the raw list of dicts directly to `tool_refs`. Pydantic coerces each dict to a string, silently dropping the override fields. ```python from cleveragents.domain.models.core.skill import Skill config = { "name": "local/my-skill", "description": "Test skill", "tools": [ { "name": "builtin/shell_execute", "description": "Execute commands in sandbox", # Override "writes": True, # Override "checkpointable": False # Override } ] } skill = Skill.from_config(config) # skill.tool_refs == ["builtin/shell_execute"] — overrides silently dropped # No error is raised ``` ## Expected Behavior Per `docs/schema/skill.schema.yaml`, tool references are objects with override fields: ```yaml tools: type: array items: type: object required_fields: [name] fields: name: type: string required: true description: type: string required: false description: "Override the tool's registered description within this skill context." writes: type: boolean required: false description: "Override the tool's writes capability flag." checkpointable: type: boolean required: false description: "Override the tool's checkpointable capability flag." ``` `Skill.from_config()` should parse each tool reference dict into a structured `SkillToolRef` domain object that preserves all override fields. The `SkillResolver` should then apply these overrides when assembling the skill's tool set. ## Code Locations - `src/cleveragents/domain/models/core/skill.py` line 239 — `tool_refs: list[str]` (no override support) - `src/cleveragents/domain/models/core/skill.py` line 340 — `from_config()` passes raw list to `tool_refs` - `docs/schema/skill.schema.yaml` — documents `description`, `writes`, `checkpointable` override fields on tool refs - `src/cleveragents/skills/schema.py` `SkillToolRefSchema` — correctly models override fields (reference for correct behaviour) ## Subtasks - [ ] Add a `SkillToolRef` dataclass or Pydantic model to `src/cleveragents/domain/models/core/skill.py` with fields: `name: str`, `description: str | None`, `writes: bool | None`, `checkpointable: bool | None` - [ ] Change `tool_refs: list[str]` to `tool_refs: list[SkillToolRef]` in the `Skill` domain model - [ ] Update `Skill.from_config()` to parse each tool reference entry (whether a plain string or a dict) into a `SkillToolRef` object, preserving all override fields - [ ] Update `SkillResolver` (and any other consumers of `skill.tool_refs`) to read override fields from `SkillToolRef` objects when populating per-tool overrides - [ ] Write Behave test scenarios verifying that `description`, `writes`, and `checkpointable` override fields are preserved end-to-end through `from_config()` - [ ] Write Behave test scenario verifying backward compatibility: plain string tool refs (no overrides) still parse correctly - [ ] Write negative test: verify that a tool ref dict missing the required `name` field raises a validation error - [ ] Run `nox -e lint` and `nox -e typecheck` to verify no regressions - [ ] Run `nox -e unit_tests` and `nox -e coverage_report` to confirm coverage >= 97% ## Definition of Done - [ ] `Skill.tool_refs` stores `SkillToolRef` objects that carry all override fields defined in `docs/schema/skill.schema.yaml` - [ ] `Skill.from_config()` correctly parses both plain-string and dict-format tool references from YAML-sourced config - [ ] Override fields (`description`, `writes`, `checkpointable`) are preserved and accessible on the `Skill` domain object after `from_config()` - [ ] `SkillResolver` correctly applies per-tool overrides sourced from `SkillToolRef` objects - [ ] The data path YAML → `SkillConfigSchema` → `Skill.from_config()` → `SkillResolver` is lossless for tool reference override fields - [ ] Behave tests cover positive cases (overrides preserved), backward-compat cases (plain strings), and negative cases (missing `name`) - [ ] `docs/schema/skill.schema.yaml` and `src/cleveragents/domain/models/core/skill.py` are fully consistent with each other - [ ] All nox stages pass - [ ] Coverage >= 97% --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
#392 Epic: Actor YAML & Compiler
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#3923
No description provided.