feat(resource): add resource registry and DAG metadata #384
@@ -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()
|
||||
@@ -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
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user