feat(resource): add resource type inheritance and polymorphic tool matching
Implement ADR-042 single-inheritance for resource types with polymorphic tool and handler resolution. Core changes: - Add `inherits` field to ResourceTypeConfigSchema, ResourceTypeSpec, and ResourceTypeModel with chain depth validation (max 5), circular inheritance detection, and built-in-from-custom guard - New inheritance.py module (390 lines): resolve_inheritance_chain(), validate_chain(), is_subtype_of(), resolve_fields(), find_subtypes() - Wire inheritance into ResourceRegistryService: chain validation on register_type(), resolve_type_inheritance_chain(), is_subtype_of() - Add find_tools_for_resource() to ToolRegistry with polymorphic matching that walks the inheritance chain - Add resolve_handler_polymorphic() to handler resolver - Alembic migration m6_004_resource_type_inherits adds inherits column and index to resource_types table CLI changes: - `agents resource type list` shows Inherits column - `agents resource type show` displays inheritance chain Tests: - 30 BDD scenarios in resource_type_inheritance.feature (all pass) - 18 Robot Framework test cases - 14 ASV benchmark timing methods across 4 suites Docs: - docs/reference/resource_type_inheritance.md (full reference) - Updated docs/schema/resource_type.schema.yaml - CHANGELOG entry for #513 ISSUES CLOSED: #513
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# Resource Type Inheritance
|
||||
|
||||
## Overview
|
||||
|
||||
Resource type inheritance allows custom resource types to derive from existing
|
||||
types, inheriting their field definitions, CLI arguments, DAG constraints, and
|
||||
tool bindings. A child type can override or extend its parent's configuration
|
||||
while remaining compatible with tools and handlers that target the parent type.
|
||||
|
||||
This feature was introduced in [ADR-042](../adr/ADR-042-resource-type-inheritance.md)
|
||||
to support polymorphic tool matching and reduce configuration duplication across
|
||||
related resource types.
|
||||
|
||||
**Key benefits:**
|
||||
|
||||
- Define specialised resource types without repeating shared configuration.
|
||||
- Tools bound to a parent type automatically match resources of any subtype.
|
||||
- Handler resolution falls back through the inheritance chain when a
|
||||
type-specific handler is not registered.
|
||||
- DAG queries respect the type hierarchy, returning resources whose type is a
|
||||
subtype of the requested ancestor.
|
||||
|
||||
## The `inherits` Field
|
||||
|
||||
A resource type declares its parent by setting the `inherits` field to the name
|
||||
of an existing resource type:
|
||||
|
||||
```yaml
|
||||
name: myorg/python-repo
|
||||
inherits: git-checkout
|
||||
resource_kind: physical
|
||||
sandbox_strategy: git_worktree
|
||||
description: A Git checkout containing a Python project.
|
||||
cli_args:
|
||||
- name: python-version
|
||||
type: string
|
||||
required: false
|
||||
description: Expected Python version.
|
||||
```
|
||||
|
||||
In this example `myorg/python-repo` inherits all fields from `git-checkout` and
|
||||
adds a `python-version` CLI argument.
|
||||
|
||||
The `inherits` field is optional. When omitted the type is a root type with no
|
||||
parent.
|
||||
|
||||
## Inheritance Chain Rules
|
||||
|
||||
The following five rules govern inheritance chains. Violations are reported by
|
||||
`validate_chain()` and prevent the type from being registered.
|
||||
|
||||
### 1. Single Inheritance Only
|
||||
|
||||
A resource type may declare at most one parent via `inherits`. Multiple
|
||||
inheritance is not supported.
|
||||
|
||||
### 2. Maximum Depth of 5 Levels
|
||||
|
||||
The inheritance chain from a type to its root ancestor must not exceed 5 levels
|
||||
(i.e., at most 4 `inherits` hops). Deeper chains raise
|
||||
`ResourceTypeInheritanceDepthError`.
|
||||
|
||||
```
|
||||
root -> A -> B -> C -> D # depth 5 — OK
|
||||
root -> A -> B -> C -> D -> E # depth 6 — rejected
|
||||
```
|
||||
|
||||
### 3. No Circular Inheritance
|
||||
|
||||
An inheritance chain must not contain cycles. If registering type `A` with
|
||||
`inherits: B` would create a cycle (e.g., `B` already inherits from `A`,
|
||||
directly or transitively), `ResourceTypeCircularInheritanceError` is raised.
|
||||
|
||||
### 4. Built-in Types May Not Inherit from Custom Types
|
||||
|
||||
Built-in resource types (`built_in: true`) are not permitted to set `inherits`
|
||||
to a custom (namespaced) type. Custom types may inherit from built-in types or
|
||||
from other custom types.
|
||||
|
||||
### 5. Cannot Remove a Parent While Subtypes Exist
|
||||
|
||||
A resource type's `inherits` field cannot be cleared or changed to a different
|
||||
parent while other registered types list it as their parent. Attempting to do so
|
||||
raises `ResourceTypeParentRemovalError`. Remove or re-parent all subtypes first.
|
||||
|
||||
## Field Resolution
|
||||
|
||||
When a type declares `inherits`, its effective configuration is computed by
|
||||
overlaying the child's explicitly declared scalar fields on top of the parent's
|
||||
resolved fields (which may themselves be inherited). This is a simple
|
||||
**last-writer-wins** override for scalar values:
|
||||
|
||||
| Field | Resolution |
|
||||
|--------------------|---------------------------------------------------|
|
||||
| `description` | Child value wins if set, otherwise parent's value. |
|
||||
| `resource_kind` | Child value wins if set, otherwise parent's value. |
|
||||
| `sandbox_strategy` | Child value wins if set, otherwise parent's value. |
|
||||
| `user_addable` | Child value wins if set, otherwise parent's value. |
|
||||
| `handler` | Child value wins if set, otherwise parent's value. |
|
||||
| `capabilities` | Child value wins if set, otherwise parent's value. |
|
||||
| `auto_discovery` | Child value wins if set, otherwise parent's value. |
|
||||
| `equivalence` | Child value wins if set, otherwise parent's value. |
|
||||
|
||||
The `name`, `built_in`, and `inherits` fields are never inherited — they are
|
||||
intrinsic to the declaring type.
|
||||
|
||||
## Collection Field Merging
|
||||
|
||||
Collection fields use **additive merging** by default: the child's entries are
|
||||
appended to the parent's resolved entries (duplicates are deduplicated). To
|
||||
replace the parent's collection entirely, set the corresponding
|
||||
`<field>_replace` flag to `true`.
|
||||
|
||||
### `cli_args`
|
||||
|
||||
Parent CLI arguments are included in the child type. The child may add new
|
||||
arguments or override a parent argument by declaring an argument with the same
|
||||
`name`. To discard all parent arguments and use only the child's definitions:
|
||||
|
||||
```yaml
|
||||
cli_args_replace: true
|
||||
cli_args:
|
||||
- name: custom-arg
|
||||
type: string
|
||||
```
|
||||
|
||||
### `child_types`
|
||||
|
||||
The child type's allowed `child_types` list is the union of the parent's and
|
||||
the child's declarations. To replace:
|
||||
|
||||
```yaml
|
||||
child_types_replace: true
|
||||
child_types:
|
||||
- myorg/sub-resource
|
||||
```
|
||||
|
||||
### `parent_types`
|
||||
|
||||
Behaves identically to `child_types` — additive merge with
|
||||
`parent_types_replace: true` for full replacement.
|
||||
|
||||
## Polymorphism Guarantees
|
||||
|
||||
### Tool Binding
|
||||
|
||||
`ToolRegistry.find_tools_for_resource()` returns tools bound to the resource's
|
||||
own type **and** every ancestor type in its inheritance chain. A tool registered
|
||||
for `git-checkout` will match resources of type `myorg/python-repo` if
|
||||
`myorg/python-repo` inherits from `git-checkout`.
|
||||
|
||||
### Handler Resolution
|
||||
|
||||
When resolving a handler for a resource, the system walks the inheritance chain
|
||||
from the most specific type to the root. The first type in the chain that has a
|
||||
registered handler is used. This allows specialised types to override behaviour
|
||||
while falling back to the parent handler when no override exists.
|
||||
|
||||
### DAG Queries
|
||||
|
||||
Resource DAG queries that filter by type respect the inheritance hierarchy. A
|
||||
query for all resources of type `git-checkout` also returns resources whose type
|
||||
is a subtype of `git-checkout`. Use `is_subtype_of()` for explicit type checks.
|
||||
|
||||
## API Reference
|
||||
|
||||
Public functions exported from `cleveragents.resource.inheritance`:
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `resolve_inheritance_chain` | `(type_name: str, type_registry: dict) -> list[str]` | Returns the ordered inheritance chain from `type_name` to the root ancestor (inclusive). Raises `ResourceTypeCircularInheritanceError` on cycles and `ResourceTypeParentNotFoundError` if an ancestor is missing. |
|
||||
| `validate_chain` | `(type_name: str, inherits: str \| None, type_registry: dict, is_built_in: bool) -> list[str]` | Validates the proposed inheritance chain and returns a list of error messages (empty on success). Checks all five chain rules. |
|
||||
| `is_subtype_of` | `(type_name: str, ancestor_name: str, type_registry: dict) -> bool` | Returns `True` if `type_name` is equal to or a subtype of `ancestor_name`. |
|
||||
| `resolve_fields` | `(type_name: str, type_registry: dict) -> dict` | Returns the fully resolved field dictionary for `type_name` after applying inheritance chain field resolution and collection merging. |
|
||||
| `find_subtypes` | `(ancestor_name: str, type_registry: dict) -> list[str]` | Returns all registered type names that are subtypes of `ancestor_name` (direct and transitive). |
|
||||
|
||||
## Exceptions
|
||||
|
||||
The following exceptions are defined in `cleveragents.resource.inheritance` and
|
||||
raised when inheritance rules are violated:
|
||||
|
||||
| Exception | Raised When |
|
||||
|-----------|-------------|
|
||||
| `ResourceTypeInheritanceDepthError` | The inheritance chain exceeds the maximum depth of 5 levels. |
|
||||
| `ResourceTypeCircularInheritanceError` | A circular inheritance chain is detected. |
|
||||
| `ResourceTypeParentNotFoundError` | The type named in `inherits` is not present in the type registry. |
|
||||
| `ResourceTypeParentRemovalError` | An attempt is made to remove or change a type's parent while subtypes still reference it. |
|
||||
|
||||
All exceptions inherit from `CleverAgentsError` and include the type name and
|
||||
chain details in their message for diagnostics.
|
||||
|
||||
## CLI
|
||||
|
||||
### `agents resource type list`
|
||||
|
||||
The tabular output includes an **Inherits** column showing the direct parent
|
||||
type name (blank for root types):
|
||||
|
||||
```
|
||||
Name Kind Sandbox Inherits
|
||||
────────────────────── ───────── ──────────────── ──────────────
|
||||
git-checkout physical git_worktree
|
||||
fs-directory physical copy_on_write
|
||||
myorg/python-repo physical git_worktree git-checkout
|
||||
```
|
||||
|
||||
### `agents resource type show <name>`
|
||||
|
||||
Displays the full resolved configuration for the type, including an
|
||||
**Inheritance Chain** section:
|
||||
|
||||
```
|
||||
Name: myorg/python-repo
|
||||
Inherits: git-checkout
|
||||
Inheritance Chain: myorg/python-repo -> git-checkout
|
||||
Resource Kind: physical
|
||||
Sandbox Strategy: git_worktree
|
||||
...
|
||||
```
|
||||
|
||||
## Database
|
||||
|
||||
The `resource_types` table includes an `inherits` column:
|
||||
|
||||
| Column | Type | Nullable | Description |
|
||||
|------------|---------------|----------|------------------------------------------|
|
||||
| `inherits` | `String(255)` | Yes | Name of the parent resource type, or `NULL` for root types. |
|
||||
|
||||
This column is added by the Alembic migration
|
||||
`m6_004_resource_type_inherits`. The column is indexed to support efficient
|
||||
subtype lookups. Foreign-key integrity is enforced at the application layer
|
||||
(not via a database constraint) because type registration order during
|
||||
migrations may not satisfy FK ordering requirements.
|
||||
Reference in New Issue
Block a user