Boundary fix: - Delete src/cleveractors/acms/index.py (ACMSIndex, FileTraversalEngine, IndexEntry, FileType, TierLevel) — these are CLI/storage concerns that belong in cleveragents-core per ADR-001; the file already exists there at cleveragents/acms/index.py - Clean src/cleveractors/acms/__init__.py: remove all index.py imports and re-exports; __all__ now derives purely from uko.__all__ ProviderRegistryPort (ADR-005): - Add src/cleveractors/ports/provider_registry.py: ProviderRegistryPort Protocol with get(provider, model) -> Agent | None; structural typing, no host imports, mirrors the shape of ToolRegistryPort - Update src/cleveractors/ports/__init__.py: export ProviderRegistryPort alongside ToolRegistryPort; update module docstring Wire provider resolution into the node executor: - compiler.py: _map_node now accepts actor_provider/actor_model defaults and merges them into AGENT node metadata (setdefault so per-node config values still win); compile_actor passes config.provider/config.model - nodes.py: Node.__init__ gains optional provider_registry parameter; _execute_agent resolution order is now: (1) pre-resolved agents dict, (2) ProviderRegistryPort.get(provider, model) from node metadata, (3) graceful synthetic fallback — the ValueError guard for missing config.agent is removed since the registry is a valid alternative path Documentation: - Port ADR-003 (actor abstraction definition) from cleveragents-core ADR-031 - Port ADR-004 (Jinja2 YAML template preprocessing) from cleveragents-core ADR-032 - Add ADR-005 (Provider Registry Protocol) for the new provider_registry port - Port five reference docs: actors_schema.md, actor_compiler.md, actor_config.md, actor_hierarchy.md, actors_examples.md - Port API reference: api/actor.md - Add provider field to all YAML examples in actors_examples.md, actor_hierarchy.md, and actor_config.md - Update error messages in actor_config.md to match actual validator output - Add graph-level provider/model propagation docs to actor_compiler.md - Add ADR-005 cross-references to ADR-001, ADR-002, and actors_schema.md - Fix ADR-005 status section to reflect that implementation is in this PR - Fix ADR-004 to reference actual test files (smoke.feature, not phantom ones) - Remove fabricated reserved-namespace constraint from ADR-003 Constraints - Fix broken LICENSE link in docs/index.md for MkDocs rendering - Add provider field to ActorConfigSchema table in api/actor.md - Add internal modules section to docs/specification.md (ticket item 7) - Fix markdown formatting in actors_schema.md provider field definition - Add ADR-005 cross-reference to actors_schema.md provider field section BDD coverage (7 scenarios, 30 steps): - ProviderRegistryPort happy-path resolution - Graceful fallback when provider registry returns None - Graceful fallback when no provider registry is supplied - Pre-resolved agents dict takes precedence over provider registry - Compile a minimal graph actor, reject missing provider, render template Post-review fixes applied in amend: - actors_examples.md and actor_hierarchy.md: added provider to all examples - ADR-003: removed fabricated reserved-namespace constraint - ADR-004: corrected phantom test file references - ADR-005: updated status to reflect implementation is included - Added 'no registry' and 'agents dict precedence' BDD scenarios - Added provider to api/actor.md ActorConfigSchema fields table - ADR-001 and ADR-002: added ADR-005 cross-references - actor_config.md: fixed error message to match validator - actor_compiler.md: documented graph-level provider/model propagation - index.md: fixed broken LICENSE link - actors_schema.md: added ADR-005 cross-reference ISSUES CLOSED: #4
8.3 KiB
CleverActors Specification
Overview
CleverActors is a pure Python library: it parses a CleverAgents v3 actor YAML file, validates it against a Pydantic schema, optionally preprocesses it through a sandboxed Jinja2 engine, and compiles it into a LangGraph node + edge graph ready for execution by a host application.
The library carries no runtime concerns of its own — no I/O, no
persistence, no event publishing, no CLI, no HTTP, no DI container.
Everything that needs the host's state goes through the small set of
Protocols in cleveractors.ports.
Scope
Customer YAML
│
▼
┌──────────────────────┐
│ cleveractors.actor │ ← schema, loader, compiler,
│ .{loader,schema, │ role validation, Jinja2
│ compiler,config, │ preprocessing
│ yaml_loader,…} │
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ cleveractors. │ ← compile target — pure
│ langgraph.{nodes, │ data classes describing
│ state, …} │ node/edge structure
└──────────┬───────────┘
│
▼
host application
(cleveragents-core, cleverrouter,
third-party) drives the resulting
graph through whatever execution
model it chooses.
Public surface
from cleveractors.actor.schema import ActorConfigSchema
from cleveractors.actor.compiler import compile_actor, CompiledActor
from cleveractors.actor.loader import ActorLoader
from cleveractors.actor.config import ActorConfiguration
from cleveractors.actor import (
ActorCompilationError,
SubgraphCycleError,
MissingNodeError,
InvalidEntryExitError,
)
from cleveractors.langgraph.nodes import Node, Edge, NodeConfig, NodeType
from cleveractors.langgraph.state import GraphState, StateSnapshot
from cleveractors.templates.secure_renderer import SecureTemplateRenderer
from cleveractors.lsp.models import LspBinding
from cleveractors.agents.base import Agent # minimal ABC
from cleveractors.acms.uko import VocabularyRegistry
from cleveractors.core import ValidationError, NotFoundError
from cleveractors.ports import ToolRegistryPort, ProviderRegistryPort
Extension points (Protocols)
| Protocol | Where the library calls it | Host concrete |
|---|---|---|
cleveractors.ports.tool_registry.ToolRegistryPort |
ActorLoader._resolve_tools() — looks up tool references at load time and emits a warning when unknown |
cleveragents.tool.registry.ToolRegistry (cleveragents-core); cleverrouter's own tool registry; anything with a .get(name) -> spec | None method |
cleveractors.ports.provider_registry.ProviderRegistryPort |
Node._execute_agent() — resolves (provider, model) to an Agent instance at graph execution time |
cleveragents.providers.registry wrapped in an adapter; cleverrouter's provider registry; any object with a .get(provider, model) -> Agent | None method — see ADR-005 |
What is intentionally NOT in the library
These belong to the host application:
| Concern | Where it lives |
|---|---|
| Actor persistence (database) | cleveragents.application.services.actor_service |
| Decision-tree recording | cleveragents.application.services.decision_service |
| Invariant reconciliation runtime | cleveragents.actor.reconciliation |
Actor registry runtime (ActorRegistry, v3_registry) |
cleveragents.actor.{registry, v3_registry} |
| Reactive stream routing (rx, BehaviorSubject) | cleveragents.reactive |
| LangChain provider configuration | cleveragents.providers.registry |
| LSP runtime (registry, lifecycle, transport) | cleveragents.lsp.{registry, lifecycle, server, transport} |
| Plan lifecycle (Action / Strategize / Execute / Apply) | cleveragents.application |
| CLI / TUI / A2A wire protocol | cleveragents.{cli, tui, a2a} |
| ACMS indexer / file traversal engine | cleveragents.acms.index |
Internal modules (not public API)
The following modules exist in the source tree but are not part of the public API surface. They are implementation details used internally and may change without a semver bump:
| Module | Purpose |
|---|---|
cleveractors.actor.role_validation |
Validates that an actor config is appropriate for a declared role (strategy, execution, etc.) — logic used by the compiler, not exposed directly |
cleveractors.actor.utils |
Small shared helpers (name normalisation, namespace parsing) used across the actor package |
cleveractors.actor.yaml_template_engine |
YAMLTemplateEngine — the Jinja2 + env-var two-phase pipeline described in ADR-004; consumed by ActorLoader and SecureTemplateRenderer |
cleveractors.langgraph.dynamic_router |
DynamicRouter — minimal predicate-based edge selector; used internally by graph execution |
cleveractors.langgraph.message_router |
Helpers that build MESSAGE_ROUTER NodeConfig and its outgoing Edge list from a rule set |
cleveractors.langgraph.pure_graph |
PureGraph — lightweight Pydantic model for a named node/edge graph; used in tests and by the compiler before LangGraph wiring |
cleveractors.langgraph.routing_adapter |
build_route_nodes_from_stream — translates a reactive stream operator list into a NodeConfig / Edge chain; kept for migration compatibility |
Origin
Extracted from cleveragents/cleveragents-core at commit 20ad9a46 in
2026. The extraction preserves the customer-facing actor YAML format
exactly; consumers see the same ActorConfigSchema and
compile_actor() surface — only the import path changes.
The decision tree that produced the extraction is recorded in:
- ADR-001: Library Boundary and Extraction Scope
- ADR-002: Tool Registry Protocol
- ADR-003: Actor Abstraction Definition
- ADR-004: Jinja2 YAML Template Preprocessing
- ADR-005: Provider Registry Protocol
Versioning
0.x.y while the library is following cleveragents-core changes
closely. 1.0.0 will be cut once the surface has stabilised through
at least one cleveragents-core release and one cleverrouter release
that consume it.
Consumer integration patterns
cleveragents-core
The host already owns the actor lifecycle. It calls the library only for parsing + compile, and keeps its own registry/persistence/runtime.
from cleveractors.actor.loader import ActorLoader
from cleveractors.actor.compiler import compile_actor
# Host supplies its own tool registry (any object with .get(name) works).
loader = ActorLoader(search_roots=[...], tool_registry=my_tool_registry)
configs = loader.discover()
for cfg in configs:
compiled = compile_actor(cfg)
my_actor_service.persist(cfg, compiled)
cleverrouter
The cleverrouter gateway accepts an actor YAML upload, compiles it
once into an immutable IR, and serves it as a model endpoint
(model: actor/{namespace}/{name}@{version}). The library handles
the parse + compile; cleverrouter holds the compiled artefact and
runs it through its own LangGraph executor on each request.
from cleveractors.actor.compiler import compile_actor
from cleveractors.actor.schema import ActorConfigSchema
# Inside the data-plane actor.deploy handler:
cfg = ActorConfigSchema.model_validate(yaml.safe_load(yaml_text))
compiled = compile_actor(cfg)
db.session.add(ActorVersion(
actor_id=actor.id,
source_hash=hashlib.sha256(yaml_text.encode()).hexdigest(),
compiled_ir=compiled.model_dump(),
safety_report=cleverrouter_safety_pass(compiled),
))