feat(resource): add resource registry and DAG metadata #384

Merged
hamza.khyari merged 2 commits from feature/m2-resource-registry into develop-hamza-2 2026-02-23 15:51:03 +00:00
3 changed files with 416 additions and 17 deletions
+188
View File
@@ -0,0 +1,188 @@
"""ASV benchmarks for resource registry lookup operations.
Measures the performance of:
- ResourceTypeSpec construction from config dict
- ResourceRegistryService.show_type() lookup
- ResourceRegistryService.show_resource() lookup by name and ULID
- ResourceRegistryService.list_types() enumeration
- ResourceRegistryService.list_resources() enumeration
- ResourceRegistryService.register_resource() creation
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Any
try:
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.domain.models.core.resource_type import (
ResourceTypeSpec,
)
except ModuleNotFoundError:
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from cleveragents.application.services.resource_registry_service import (
ResourceRegistryService,
)
from cleveragents.domain.models.core.resource_type import (
ResourceTypeSpec,
)
def _setup_db() -> Any:
"""Create in-memory database and return session factory."""
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from cleveragents.infrastructure.database.models import Base
engine = create_engine(
"sqlite:///:memory:",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
@event.listens_for(engine, "connect")
def _fk(conn: Any, _rec: Any) -> None:
conn.cursor().execute("PRAGMA foreign_keys=ON")
Base.metadata.create_all(engine)
return sessionmaker(bind=engine)
class TypeSpecConstructionSuite:
"""Benchmark ResourceTypeSpec construction from config dicts."""
def time_from_config_minimal(self) -> None:
"""Benchmark minimal type spec construction."""
ResourceTypeSpec.from_config(
{
"name": "bench/minimal",
"description": "Minimal benchmark type",
"resource_kind": "physical",
"sandbox_strategy": "copy_on_write",
}
)
def time_from_config_full(self) -> None:
"""Benchmark full type spec construction with all fields."""
ResourceTypeSpec.from_config(
{
"name": "bench/full",
"description": "Full benchmark type",
"resource_kind": "physical",
"sandbox_strategy": "git_worktree",
"user_addable": True,
"built_in": False,
"cli_args": [
{
"name": "path",
"type": "path",
"required": True,
"description": "Path to resource",
},
{
"name": "branch",
"type": "string",
"required": False,
"description": "Branch name",
"default": "main",
},
],
"parent_types": ["git-checkout"],
"child_types": ["fs-directory", "fs-file"],
"handler": "bench.handler:BenchHandler",
"capabilities": {
"read": True,
"write": True,
"sandbox": True,
"checkpoint": False,
},
"auto_discovery": {
"enabled": True,
"rules": [
{"type": "fs-directory", "pattern": "*/"},
],
},
}
)
class RegistryLookupSuite:
"""Benchmark registry service lookup operations."""
def setup(self) -> None:
self._sf = _setup_db()
self._svc = ResourceRegistryService(session_factory=self._sf)
self._svc.bootstrap_builtin_types()
# Register some resources for lookup benchmarks
self._resources = []
for i in range(20):
r = self._svc.register_resource(
type_name="git-checkout",
name=f"bench/repo-{i}",
location=f"/tmp/bench/repo-{i}",
description=f"Benchmark repo {i}",
)
self._resources.append(r)
def time_show_type_builtin(self) -> None:
"""Benchmark looking up a built-in type by name."""
self._svc.show_type("git-checkout")
def time_show_type_second_builtin(self) -> None:
"""Benchmark looking up the second built-in type."""
self._svc.show_type("fs-directory")
def time_list_types(self) -> None:
"""Benchmark listing all registered types."""
self._svc.list_types()
def time_list_types_filtered(self) -> None:
"""Benchmark listing types filtered by namespace."""
self._svc.list_types(namespace="builtin")
def time_show_resource_by_name(self) -> None:
"""Benchmark looking up a resource by namespaced name."""
self._svc.show_resource("bench/repo-0")
def time_show_resource_by_ulid(self) -> None:
"""Benchmark looking up a resource by ULID."""
self._svc.show_resource(self._resources[0].resource_id)
def time_list_resources_all(self) -> None:
"""Benchmark listing all resources."""
self._svc.list_resources()
def time_list_resources_filtered(self) -> None:
"""Benchmark listing resources filtered by type."""
self._svc.list_resources(type_name="git-checkout")
class RegistryRegistrationSuite:
"""Benchmark resource registration throughput."""
_reg_ctr: int = 0
def setup(self) -> None:
self._sf = _setup_db()
self._svc = ResourceRegistryService(session_factory=self._sf)
self._svc.bootstrap_builtin_types()
def time_register_resource(self) -> None:
"""Benchmark registering a single resource."""
RegistryRegistrationSuite._reg_ctr += 1
self._svc.register_resource(
type_name="git-checkout",
name=f"bench/reg-{RegistryRegistrationSuite._reg_ctr}",
location=f"/tmp/bench/reg-{RegistryRegistrationSuite._reg_ctr}",
)
def time_bootstrap_builtin_types_idempotent(self) -> None:
"""Benchmark idempotent bootstrap (types already exist)."""
self._svc.bootstrap_builtin_types()
+211
View File
@@ -0,0 +1,211 @@
# Resource Registry
The Resource Registry manages **resource types** and **resource
instances** in CleverAgents. Resource types define schemas, sandbox
strategies, and handler implementations. Resource instances are
registered entries that plans operate on during execution.
## Built-in Resource Types
CleverAgents ships with two built-in types, available without
registration:
| Type | Kind | Sandbox Strategy | Handler |
|------------------|----------|------------------|--------------------------------|
| `git-checkout` | physical | `git_worktree` | `GitCheckoutHandler` |
| `fs-directory` | physical | `copy_on_write` | `FsDirectoryHandler` |
Built-in types are registered idempotently at startup via
`ResourceRegistryService.bootstrap_builtin_types()`.
## Resource Type Fields
Each `ResourceTypeSpec` defines a resource type schema:
| Field | Type | Description |
|--------------------|---------------------|------------------------------------------------------|
| `name` | `str` | Unique name (built-in: bare, custom: `namespace/name`) |
| `description` | `str` | Human-readable description |
| `resource_kind` | `ResourceKind` | `physical` or `virtual` |
| `sandbox_strategy` | `SandboxStrategy` | Default sandbox isolation strategy |
| `user_addable` | `bool` | Whether users can register instances of this type |
| `built_in` | `bool` | Whether this is a built-in type |
| `cli_args` | `list[ResourceTypeArgument]` | CLI argument definitions for registration |
| `parent_types` | `list[str]` | Allowed parent type names in the DAG |
| `child_types` | `list[str]` | Allowed child type names in the DAG |
| `handler` | `str \| None` | Handler ref in `module:Class` format |
| `capabilities` | `dict[str, bool]` | `read`, `write`, `sandbox`, `checkpoint` flags |
| `auto_discovery` | `dict \| None` | Auto-discovery rules (see below) |
| `equivalence` | `dict \| None` | Virtual resource deduplication config |
### Custom Types
Custom types are defined in YAML and registered via CLI or API:
```yaml
name: myteam/python-package
description: A Python package directory
resource_kind: physical
sandbox_strategy: copy_on_write
user_addable: true
cli_args:
- name: path
type: path
required: true
description: Path to the package root
parent_types:
- git-checkout
child_types:
- fs-directory
```
Register with: `agents resource type add path/to/type.yaml`
## Resource Instance Fields
Each `Resource` represents a registered asset:
| Field | Type | Description |
|----------------------|-----------------------|----------------------------------------------|
| `resource_id` | `str` (ULID) | Unique identifier |
| `name` | `str \| None` | Optional namespaced name |
| `resource_type_name` | `str` | Type this resource belongs to |
| `classification` | `PhysVirt` | `physical` or `virtual` |
| `description` | `str \| None` | Human-readable description |
| `location` | `str \| None` | Filesystem path (physical resources) |
| `sandbox_strategy` | `SandboxStrategy \| None` | Per-resource sandbox override |
| `content_hash` | `str \| None` | Hash of resource contents for change detection |
| `parents` | `list[str]` | Parent resource IDs in the DAG |
| `children` | `list[str]` | Child resource IDs in the DAG |
| `properties` | `dict` | Type-specific key-value properties |
| `capabilities` | `ResourceCapabilities`| `readable`, `writable`, `sandboxable`, `checkpointable` |
| `created_at` | `datetime` | Registration timestamp |
| `updated_at` | `datetime` | Last modification timestamp |
## Registry Service API
### Type Operations
| Method | Description |
|-------------------------|------------------------------------------|
| `bootstrap_builtin_types()` | Register built-in types (idempotent) |
| `register_type(config_path)` | Register a custom type from YAML |
| `list_types(namespace=)` | List types, optionally filtered |
| `show_type(name)` | Get a type by name |
### Resource Operations
| Method | Description |
|-------------------------------|--------------------------------------|
| `register_resource(type_name, name=, location=, ...)` | Create and register an instance |
| `list_resources(type_name=)` | List resources, optionally by type |
| `show_resource(name_or_id)` | Get by namespaced name or ULID |
### DAG Operations
| Method | Description |
|---------------------------------|------------------------------------|
| `link_child(parent, child)` | Link a child to a parent |
| `unlink_child(parent, child)` | Remove a parent-child link |
| `get_children(name_or_id)` | Get direct children |
| `get_resource_tree(name_or_id, depth=, type_filter=)` | Recursive tree traversal |
For DAG rules, cycle detection, type compatibility, and auto-discovery
details, see [Resource DAG](resource_dag.md).
## Discovery Behavior
### Auto-Discovery
Resource types can define auto-discovery rules that materialize child
resources automatically:
```yaml
auto_discovery:
enabled: true
rules:
- type: fs-directory
pattern: "*/"
- type: fs-file
pattern: "*"
```
When `auto_discover_children(resource_id)` is called:
1. The resource and its type are looked up.
2. Each discovery rule is evaluated:
- The child type must exist in the registry.
- The child type must be compatible with the parent type (per
`child_types`).
3. New child resources are created with `auto_discovered = true`.
4. Links are created in the `resource_links` table.
Auto-discovery is triggered:
- When a resource is first registered
- When resource contents change
- On demand via CLI (`agents resource discover`)
### Local-Mode Discovery
In local mode (the default), discovery operates against the local
filesystem. The registry creates placeholder resource entries based
on discovery rules; actual file enumeration is delegated to resource
handlers at sandbox creation time.
## Database Schema
### `resource_types` Table
| Column | Type | Description |
|---------------------------|---------------|--------------------------------|
| `name` | `String(128)` | Primary key |
| `namespace` | `String(64)` | Namespace (or `builtin`) |
| `description` | `Text` | Human-readable description |
| `resource_kind` | `String(16)` | `physical` or `virtual` |
| `sandbox_strategy` | `String(32)` | Default sandbox strategy |
| `user_addable` | `Boolean` | User registration allowed |
| `built_in` | `Boolean` | Built-in type flag |
| `handler` | `String(256)` | Handler `module:Class` ref |
| `args_schema_json` | `Text` | CLI args JSON |
| `allowed_parent_types_json` | `Text` | Allowed parent types JSON |
| `allowed_child_types_json` | `Text` | Allowed child types JSON |
| `auto_discover_json` | `Text` | Auto-discovery config JSON |
| `capabilities_json` | `Text` | Capabilities JSON |
| `equivalence_json` | `Text` | Equivalence config JSON |
| `source` | `String(256)` | Registration source |
| `created_at` | `String(30)` | ISO-8601 timestamp |
| `updated_at` | `String(30)` | ISO-8601 timestamp |
### `resources` Table
| Column | Type | Description |
|--------------------|---------------|-------------------------------------|
| `resource_id` | `String(26)` | ULID primary key |
| `namespaced_name` | `String(256)` | Optional namespaced name (unique) |
| `namespace` | `String(64)` | Namespace extracted from name |
| `type_name` | `String(128)` | FK to `resource_types.name` |
| `resource_kind` | `String(16)` | `physical` or `virtual` |
| `location` | `Text` | Filesystem path |
| `description` | `Text` | Human-readable description |
| `read_only` | `Boolean` | Read-only flag |
| `auto_discovered` | `Boolean` | Created by auto-discovery |
| `sandbox_strategy` | `String(32)` | Per-resource sandbox override |
| `content_hash` | `String(128)` | Content hash for change detection |
| `properties_json` | `Text` | Type-specific properties JSON |
| `metadata_json` | `Text` | Additional metadata JSON |
| `created_at` | `String(30)` | ISO-8601 timestamp |
| `updated_at` | `String(30)` | ISO-8601 timestamp |
### `resource_links` Table
See [Resource DAG](resource_dag.md#database-schema) for link table
schema.
## Source
- Domain models: `src/cleveragents/domain/models/core/resource.py`,
`resource_type.py`
- Service: `src/cleveragents/application/services/resource_registry_service.py`
- Migration: `alembic/versions/b1_001_resource_registry_tables.py`
- CLI: `src/cleveragents/cli/commands/resource.py`
- Specification: `docs/specification.md` — Resource Registry sections
+17 -17
View File
@@ -2405,23 +2405,23 @@ This section replaces all unfinished work items with the Day 14 plan. Work is or
- [ ] Git [Jeff]: `git push -u origin feature/m3-agent-skills-registry`
- [ ] Forgejo PR [Jeff]: Open PR from `feature/m3-agent-skills-registry` to `master` with a suitable and thorough description
- [ ] **COMMIT (Owner: Hamza | Group: M2.5.resource-registry | Branch: feature/m2-resource-registry | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(resource): add resource registry and DAG metadata"**
- [ ] Git [Hamza]: `git checkout master`
- [ ] Git [Hamza]: `git pull origin master`
- [ ] Git [Hamza]: `git checkout -b feature/m2-resource-registry`
- [ ] Code [Hamza]: Implement ResourceType and Resource registries with DAG parent/child constraints and discovery metadata.
- [ ] Code [Hamza]: Add local-mode discovery stubs and validation hooks for resource graph integrity.
- [ ] Docs [Hamza]: Update `docs/reference/resources.md` with registry fields and discovery behavior.
- [ ] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit)
- [ ] Tests (Behave) [Hamza]: Add scenarios for registry validation, DAG constraints, and discovery defaults.
- [ ] Tests (Robot) [Hamza]: Add Robot test verifying resource registry CLI output.
- [ ] Tests (ASV) [Hamza]: Add `benchmarks/resource_registry_bench.py` for registry lookup overhead.
- [ ] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
- [ ] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
- [ ] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
- [ ] Git [Hamza]: `git commit -m "feat(resource): add resource registry and DAG metadata"`
- [ ] Git [Hamza]: `git push -u origin feature/m2-resource-registry`
- [ ] Forgejo PR [Hamza]: Open PR from `feature/m2-resource-registry` to `master` with a suitable and thorough description
- [x] **COMMIT (Owner: Hamza | Group: M2.5.resource-registry | Branch: feature/m2-resource-registry | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(resource): add resource registry and DAG metadata"**
- [x] Git [Hamza]: `git checkout master`
- [x] Git [Hamza]: `git pull origin master`
- [x] Git [Hamza]: `git checkout -b feature/m2-resource-registry`
- [x] Code [Hamza]: Implement ResourceType and Resource registries with DAG parent/child constraints and discovery metadata.
- [x] Code [Hamza]: Add local-mode discovery stubs and validation hooks for resource graph integrity.
- [x] Docs [Hamza]: Update `docs/reference/resources.md` with registry fields and discovery behavior.
- [x] Git [Hamza]: `git fetch origin && git merge origin/master` (run before final tests and before commit)
- [x] Tests (Behave) [Hamza]: Add scenarios for registry validation, DAG constraints, and discovery defaults.
- [x] Tests (Robot) [Hamza]: Add Robot test verifying resource registry CLI output.
- [x] Tests (ASV) [Hamza]: Add `benchmarks/resource_registry_bench.py` for registry lookup overhead.
- [x] Quality [Hamza]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
- [x] Quality [Hamza]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
- [x] Git [Hamza]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
- [x] Git [Hamza]: `git commit -m "feat(resource): add resource registry and DAG metadata"`
- [x] Git [Hamza]: `git push -u origin feature/m2-resource-registry`
- [x] Forgejo PR [Hamza]: Open PR from `feature/m2-resource-registry` to `master` with a suitable and thorough description
- [ ] **COMMIT (Owner: Luis | Group: M2.6.changeset-persistence | Branch: feature/m2-changeset-persistence | Planned: Day 16 | Expected: Day 18) - Commit message: "feat(changeset): persist changesets and diff artifacts"**
- [ ] Git [Luis]: `git checkout master`