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:
2026-03-06 21:17:24 +00:00
parent fcdf80f342
commit bb8175aa11
20 changed files with 2991 additions and 125 deletions
+233
View File
@@ -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.