feat: merge branch 'master' into feature/m3-skill-cli

This commit is contained in:
2026-02-17 22:45:44 +00:00
13 changed files with 4186 additions and 1 deletions
+358
View File
@@ -0,0 +1,358 @@
"""ASV benchmarks for Actor YAML schema validation throughput.
Measures the performance of:
- YAML string parsing + schema validation
- YAML file loading + schema validation
- Graph topology validation (cycle detection)
- Tool definition validation
- model_dump() serialization
"""
from __future__ import annotations
import importlib
import sys
import tempfile
from pathlib import Path
# Ensure the local *source* tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
# Force-reload the top-level package
import cleveragents # noqa: E402
importlib.reload(cleveragents)
from cleveragents.actor.schema import ActorConfigSchema # noqa: E402
_MINIMAL_LLM_YAML = """\
name: bench/llm
type: llm
description: Benchmark LLM actor
model: gpt-4
"""
_FULL_LLM_YAML = """\
name: bench/llm_full
type: llm
description: Full LLM actor with all features
version: "1.0"
model: gpt-4-turbo
system_prompt: |
You are a benchmark testing assistant.
Process requests efficiently and accurately.
tools:
- files/read_file
- files/write_file
context_view: executor
memory:
enabled: true
max_messages: 100
max_tokens: 8000
summarize_old: true
context:
include_files:
- README.md
- pyproject.toml
include_dirs:
- src/
- tests/
exclude_patterns:
- "**/__pycache__/**"
- "*.pyc"
max_context_tokens: 16000
env_vars:
LOG_LEVEL: info
WORK_DIR: /tmp/bench
"""
_TOOL_ACTOR_YAML = """\
name: bench/tools
type: tool
description: Tool collection for benchmarking
tools:
- files/read_file
- files/write_file
- files/delete_file
"""
_SIMPLE_GRAPH_YAML = """\
name: bench/simple_graph
type: graph
description: Simple 3-node graph for benchmarking
model: gpt-4
route:
nodes:
- id: extract
type: tool
name: Extractor
description: Extract data
config:
tool_name: data/extract
- id: process
type: agent
name: Processor
description: Process data
config:
prompt: "Process the data"
- id: save
type: tool
name: Saver
description: Save results
config:
tool_name: data/save
edges:
- from_node: extract
to_node: process
- from_node: process
to_node: save
entry_node: extract
exit_nodes:
- save
"""
_COMPLEX_GRAPH_YAML = """\
name: bench/complex_graph
type: graph
description: Complex graph with 10 nodes and conditionals
model: gpt-4
route:
nodes:
- id: start
type: agent
name: Starter
description: Start node
config:
prompt: "Begin"
- id: node2
type: agent
name: Node 2
description: Second node
config:
prompt: "Continue"
- id: node3
type: agent
name: Node 3
description: Third node
config:
prompt: "Process"
- id: checker
type: conditional
name: Checker
description: Check condition
config:
conditions:
- check: "state.get('ok') == True"
route_to: node4
- check: "state.get('ok') == False"
route_to: node5
- id: node4
type: agent
name: Node 4
description: Success path
config:
prompt: "Success"
- id: node5
type: agent
name: Node 5
description: Retry path
config:
prompt: "Retry"
- id: node6
type: tool
name: Tool 6
description: Tool execution
config:
tool_name: test/tool
- id: node7
type: agent
name: Node 7
description: Seventh node
config:
prompt: "Continue"
- id: node8
type: subgraph
name: Subgraph
description: Nested workflow
config:
actor_path: bench/nested.yaml
- id: end
type: agent
name: End
description: Final node
config:
prompt: "Complete"
edges:
- from_node: start
to_node: node2
- from_node: node2
to_node: node3
- from_node: node3
to_node: checker
- from_node: node4
to_node: node6
- from_node: node5
to_node: node7
- from_node: node6
to_node: node8
- from_node: node7
to_node: node8
- from_node: node8
to_node: end
entry_node: start
exit_nodes:
- end
"""
class TimeActorSchemaMinimalLLM:
"""Benchmark minimal LLM actor validation."""
def time_parse_minimal_llm(self) -> None:
"""Time parsing a minimal LLM actor YAML."""
import yaml
data = yaml.safe_load(_MINIMAL_LLM_YAML)
ActorConfigSchema.model_validate(data)
class TimeActorSchemaFullLLM:
"""Benchmark full LLM actor validation."""
def time_parse_full_llm(self) -> None:
"""Time parsing a full LLM actor YAML with all features."""
import yaml
data = yaml.safe_load(_FULL_LLM_YAML)
ActorConfigSchema.model_validate(data)
class TimeActorSchemaTool:
"""Benchmark tool actor validation."""
def time_parse_tool_actor(self) -> None:
"""Time parsing a tool actor YAML."""
import yaml
data = yaml.safe_load(_TOOL_ACTOR_YAML)
ActorConfigSchema.model_validate(data)
class TimeActorSchemaSimpleGraph:
"""Benchmark simple graph actor validation."""
def time_parse_simple_graph(self) -> None:
"""Time parsing a simple 3-node graph actor."""
import yaml
data = yaml.safe_load(_SIMPLE_GRAPH_YAML)
ActorConfigSchema.model_validate(data)
class TimeActorSchemaComplexGraph:
"""Benchmark complex graph actor validation."""
def time_parse_complex_graph(self) -> None:
"""Time parsing a complex 10-node graph with conditionals."""
import yaml
data = yaml.safe_load(_COMPLEX_GRAPH_YAML)
ActorConfigSchema.model_validate(data)
class TimeActorSchemaFileIO:
"""Benchmark file I/O operations."""
def setup(self) -> None:
"""Create temporary YAML file."""
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False
) as temp_file:
temp_file.write(_FULL_LLM_YAML)
self.temp_file_name = temp_file.name
def teardown(self) -> None:
"""Remove temporary file."""
Path(self.temp_file_name).unlink(missing_ok=True)
def time_load_from_file(self) -> None:
"""Time loading actor config from YAML file."""
ActorConfigSchema.from_yaml_file(self.temp_file_name)
def time_save_to_file(self) -> None:
"""Time saving actor config to YAML file."""
import yaml
data = yaml.safe_load(_FULL_LLM_YAML)
config = ActorConfigSchema.model_validate(data)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".yaml", delete=False
) as temp_out:
temp_out_name = temp_out.name
try:
config.to_yaml_file(temp_out_name)
finally:
Path(temp_out_name).unlink(missing_ok=True)
class TimeActorSchemaSerialization:
"""Benchmark serialization operations."""
def setup(self) -> None:
"""Parse actor configs once."""
import yaml
self.minimal_config = ActorConfigSchema.model_validate(
yaml.safe_load(_MINIMAL_LLM_YAML)
)
self.full_config = ActorConfigSchema.model_validate(
yaml.safe_load(_FULL_LLM_YAML)
)
self.graph_config = ActorConfigSchema.model_validate(
yaml.safe_load(_COMPLEX_GRAPH_YAML)
)
def time_dump_minimal(self) -> None:
"""Time model_dump() on minimal config."""
self.minimal_config.model_dump(mode="json")
def time_dump_full(self) -> None:
"""Time model_dump() on full config."""
self.full_config.model_dump(mode="json")
def time_dump_graph(self) -> None:
"""Time model_dump() on complex graph."""
self.graph_config.model_dump(mode="json")
class TimeActorSchemaGraphValidation:
"""Benchmark graph topology validation."""
def setup(self) -> None:
"""Parse graph configs."""
import yaml
self.simple_graph_data = yaml.safe_load(_SIMPLE_GRAPH_YAML)
self.complex_graph_data = yaml.safe_load(_COMPLEX_GRAPH_YAML)
def time_validate_simple_graph_topology(self) -> None:
"""Time validation of simple graph topology."""
ActorConfigSchema.model_validate(self.simple_graph_data)
def time_validate_complex_graph_topology(self) -> None:
"""Time validation of complex graph with cycle detection."""
ActorConfigSchema.model_validate(self.complex_graph_data)
# Module-level metadata for ASV
time_parse_minimal_llm = TimeActorSchemaMinimalLLM()
time_parse_full_llm = TimeActorSchemaFullLLM()
time_parse_tool_actor = TimeActorSchemaTool()
time_parse_simple_graph = TimeActorSchemaSimpleGraph()
time_parse_complex_graph = TimeActorSchemaComplexGraph()
time_file_io = TimeActorSchemaFileIO()
time_serialization = TimeActorSchemaSerialization()
time_graph_validation = TimeActorSchemaGraphValidation()
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
# Graph Actor - Test-Driven Development Workflow
# Demonstrates a multi-node graph with conditional routing and subgraphs
name: workflows/test_driven_dev
type: graph
description: Test-driven development workflow with automated testing and feedback loops
version: "1.0"
# LLM model for agent nodes
model: gpt-4
# Graph topology
route:
# Define all nodes in the workflow
nodes:
# Entry point: planning agent
- id: planner
type: agent
name: Test Planner
description: Plans test cases based on requirements
config:
model: gpt-4
prompt: |
You are a test planning expert. Analyze the requirements and create
a comprehensive test plan covering:
- Unit tests for individual functions
- Integration tests for component interactions
- Edge cases and error conditions
tools:
- files/read_file
- files/list_directory
# Write tests first
- id: test_writer
type: agent
name: Test Writer
description: Writes test cases based on the plan
config:
model: gpt-4
prompt: |
You are a test writing expert. Write pytest tests based on the plan.
Follow best practices:
- Use descriptive test names
- Include docstrings
- Use fixtures appropriately
- Test one thing per test
tools:
- files/write_file
- files/read_file
# Run the tests (should fail initially)
- id: run_tests
type: tool
name: Test Runner
description: Executes pytest test suite
config:
tool_name: testing/run_pytest
parameters:
verbose: true
coverage: true
# Check test results
- id: check_results
type: conditional
name: Test Result Checker
description: Routes based on test pass/fail status
config:
conditions:
- check: "state.get('tests_passed') == True"
route_to: code_review
- check: "state.get('tests_passed') == False"
route_to: implementation_writer
# Write implementation to make tests pass
- id: implementation_writer
type: agent
name: Implementation Writer
description: Writes code to make the tests pass
config:
model: gpt-4
prompt: |
You are an implementation expert. Write clean, well-documented code
that makes the failing tests pass. Follow SOLID principles and
write maintainable code.
tools:
- files/write_file
- files/read_file
# Run tests again after implementation
- id: rerun_tests
type: tool
name: Test Rerunner
description: Re-executes tests after implementation
config:
tool_name: testing/run_pytest
parameters:
verbose: true
coverage: true
# Check if tests pass now
- id: verify_tests
type: conditional
name: Test Verification
description: Verify tests pass after implementation
config:
conditions:
- check: "state.get('tests_passed') == True"
route_to: code_review
- check: "state.get('tests_passed') == False and state.get('retry_count', 0) < 3"
route_to: debug_failures
- check: "state.get('tests_passed') == False and state.get('retry_count', 0) >= 3"
route_to: escalate
# Debug test failures
- id: debug_failures
type: agent
name: Debugger
description: Analyzes and fixes test failures
config:
model: gpt-4
prompt: |
You are a debugging expert. Analyze the test failures and fix the
implementation. Look for:
- Logic errors
- Edge cases
- Type mismatches
- Missing error handling
tools:
- files/read_file
- files/write_file
# Code review (subgraph)
- id: code_review
type: subgraph
name: Code Reviewer
description: Runs code review workflow
config:
actor_path: examples/actors/simple_llm.yaml
# Escalate if tests keep failing
- id: escalate
type: tool
name: Escalation Handler
description: Escalates persistent failures to human
config:
tool_name: notifications/send_alert
parameters:
channel: engineering
priority: high
# Define edges (workflow transitions)
edges:
# Linear flow from planner to test writer
- from_node: planner
to_node: test_writer
# Run tests after writing them
- from_node: test_writer
to_node: run_tests
# Check results after running tests
- from_node: run_tests
to_node: check_results
# Conditional routing from check_results
# (handled by the conditional node itself)
# Write implementation if tests fail
- from_node: implementation_writer
to_node: rerun_tests
# Verify tests after rerunning
- from_node: rerun_tests
to_node: verify_tests
# Debug if tests still fail
- from_node: debug_failures
to_node: rerun_tests
# All paths eventually lead to code review or escalation
# (handled by conditional nodes)
# Entry and exit points
entry_node: planner
exit_nodes:
- code_review
- escalate
# Context settings
context_view: strategist
memory:
enabled: true
max_messages: 100
max_tokens: 16000
summarize_old: true
context:
include_files:
- "README.md"
- "requirements.txt"
- "pyproject.toml"
include_dirs:
- "src/"
- "tests/"
exclude_patterns:
- "**/__pycache__/**"
- "*.pyc"
- "**/.pytest_cache/**"
- "**/htmlcov/**"
max_context_tokens: 32000
# Environment variables
env_vars:
PYTEST_ARGS: --verbose --cov --cov-report=html
MAX_RETRIES: "3"
+67
View File
@@ -0,0 +1,67 @@
# LLM Actor with Tools
# Demonstrates an LLM actor with access to multiple tools
name: assistants/file_analyzer
type: llm
description: Analyzes files and generates reports using file system tools
version: "1.0"
# LLM configuration
model: gpt-4-turbo
system_prompt: |
You are a file analysis assistant. Use the available tools to:
- Read and analyze file contents
- Count lines, words, and characters
- Search for patterns in files
- Generate summary reports
Always explain what you're doing before using a tool.
# Tools (mix of references and inline definitions)
tools:
# Reference to existing tool
- files/read_file
- files/list_directory
# Inline tool definition
- name: utils/count_lines
description: Count the number of lines in a file
parameters:
- name: file_path
type: str
description: Path to the file to count lines in
required: true
code: |
def count_lines(file_path: str) -> int:
"""Count lines in a file."""
with open(file_path, 'r', encoding='utf-8') as f:
return len(f.readlines())
- name: utils/word_count
description: Count words in a file
parameters:
- name: file_path
type: str
description: Path to the file
required: true
code: |
def word_count(file_path: str) -> int:
"""Count words in a file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
return len(content.split())
# Context settings
context_view: executor
memory:
enabled: true
max_messages: 30
max_tokens: 6000
context:
max_context_tokens: 10000
# Environment variables
env_vars:
WORK_DIR: ${HOME}/workspace
LOG_LEVEL: info
+78
View File
@@ -0,0 +1,78 @@
# Simple Graph Actor - Sequential Processing
# Demonstrates a simple 3-node graph with linear execution
name: workflows/document_processor
type: graph
description: Simple document processing workflow (extract → analyze → summarize)
version: "1.0"
# LLM model
model: gpt-3.5-turbo
# Graph topology
route:
nodes:
# Node 1: Extract text from document
- id: extractor
type: tool
name: Text Extractor
description: Extracts text from various document formats
config:
tool_name: documents/extract_text
parameters:
formats:
- pdf
- docx
- txt
# Node 2: Analyze content
- id: analyzer
type: agent
name: Content Analyzer
description: Analyzes document structure and content
config:
model: gpt-3.5-turbo
prompt: |
Analyze the document content and identify:
- Main topics and themes
- Key entities (people, places, organizations)
- Sentiment and tone
- Document structure
tools:
- analysis/extract_entities
- analysis/sentiment_analysis
# Node 3: Generate summary
- id: summarizer
type: agent
name: Summarizer
description: Creates concise summary of document
config:
model: gpt-3.5-turbo
prompt: |
Create a concise summary of the document including:
- Main points (3-5 bullet points)
- Key findings
- Actionable insights
Keep it under 200 words.
# Linear edges
edges:
- from_node: extractor
to_node: analyzer
- from_node: analyzer
to_node: summarizer
# Entry and exit
entry_node: extractor
exit_nodes:
- summarizer
# Context settings
context_view: executor
memory:
enabled: true
max_messages: 10
context:
max_context_tokens: 4000
+38
View File
@@ -0,0 +1,38 @@
# Simple LLM Actor Example
# Demonstrates the most basic actor configuration with just an LLM and system prompt
name: assistants/code_reviewer
type: llm
description: Reviews Python code for best practices, style, and potential bugs
version: "1.0"
# LLM configuration
model: gpt-4
system_prompt: |
You are an expert Python code reviewer. Review code for:
- PEP 8 style compliance
- Best practices and design patterns
- Potential bugs and edge cases
- Performance considerations
- Security vulnerabilities
Provide constructive feedback with specific suggestions for improvement.
# Context and memory settings
context_view: reviewer
memory:
enabled: true
max_messages: 20
max_tokens: 4000
context:
include_files:
- "README.md"
- "pyproject.toml"
include_dirs:
- "src/"
exclude_patterns:
- "**/__pycache__/**"
- "*.pyc"
- "**/.pytest_cache/**"
max_context_tokens: 8000
+45
View File
@@ -0,0 +1,45 @@
# Tool-Only Actor
# Demonstrates an actor that only provides tools without LLM interaction
name: utilities/file_operations
type: tool
description: Collection of file operation tools for other actors to use
version: "1.0"
# Tool collection
tools:
# Reference existing tools
- files/read_file
- files/write_file
- files/delete_file
- files/copy_file
- files/move_file
- files/list_directory
- files/create_directory
# Add custom validation tool
- name: validators/check_python_syntax
description: Validate Python file syntax without executing it
parameters:
- name: file_path
type: str
description: Path to Python file to validate
required: true
code: |
import ast
def check_python_syntax(file_path: str) -> dict:
"""Check if a Python file has valid syntax."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
ast.parse(f.read())
return {"valid": True, "error": None}
except SyntaxError as e:
return {
"valid": False,
"error": str(e),
"line": e.lineno,
"offset": e.offset
}
# No LLM configuration needed for tool-only actors
# No system prompt needed
+355
View File
@@ -0,0 +1,355 @@
Feature: Actor YAML schema validation
As a CleverAgents v3 user
I want to define actors via YAML configuration files
So that I can create validated, reusable actor workflows
#
# Valid LLM Actor scenarios
#
Scenario: Load a minimal valid LLM actor
Given an actor YAML string with minimal LLM configuration
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config name should be "assistants/simple"
And the actor config type should be "llm"
And the actor config model should be "gpt-4"
Scenario: Load LLM actor with system prompt
Given an actor YAML string with LLM and system prompt
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config system_prompt should contain "expert"
Scenario: Load LLM actor with tools
Given an actor YAML string with LLM and tools
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config should have 2 tools
Scenario: Load LLM actor with memory config
Given an actor YAML string with memory configuration
When I validate the actor schema
Then the actor schema validation should succeed
And the actor memory enabled should be true
And the actor memory max_messages should be 50
Scenario: Load LLM actor with context config
Given an actor YAML string with context configuration
When I validate the actor schema
Then the actor schema validation should succeed
And the actor context should include 2 files
And the actor context should include 1 directory
Scenario: Load simple_llm.yaml example
Given the actor YAML file "examples/actors/simple_llm.yaml"
When I validate the actor schema from file
Then the actor schema validation should succeed
And the actor config type should be "llm"
Scenario: Load llm_with_tools.yaml example
Given the actor YAML file "examples/actors/llm_with_tools.yaml"
When I validate the actor schema from file
Then the actor schema validation should succeed
And the actor config should have at least 2 tools
# ────────────────────────────────────────────────────────────
# Valid TOOL Actor scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load a minimal valid TOOL actor
Given an actor YAML string with TOOL type and tools
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config type should be "tool"
And the actor config should have at least 1 tool
Scenario: Load TOOL actor with inline tool definition
Given an actor YAML string with inline tool definition
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config should have 1 inline tool
Scenario: Load tool_collection.yaml example
Given the actor YAML file "examples/actors/tool_collection.yaml"
When I validate the actor schema from file
Then the actor schema validation should succeed
And the actor config type should be "tool"
# ────────────────────────────────────────────────────────────
# Valid GRAPH Actor scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load a minimal valid GRAPH actor
Given an actor YAML string with minimal GRAPH configuration
When I validate the actor schema
Then the actor schema validation should succeed
And the actor config type should be "graph"
And the actor route should have entry_node "start"
Scenario: Load GRAPH with linear topology
Given an actor YAML string with linear graph topology
When I validate the actor schema
Then the actor schema validation should succeed
And the actor route should have 3 nodes
And the actor route should have 2 edges
Scenario: Load GRAPH with conditional routing
Given an actor YAML string with conditional routing
When I validate the actor schema
Then the actor schema validation should succeed
And the actor route should have 1 conditional node
Scenario: Load GRAPH with subgraph node
Given an actor YAML string with subgraph node
When I validate the actor schema
Then the actor schema validation should succeed
And the actor route should have 1 subgraph node
Scenario: Load simple_graph.yaml example
Given the actor YAML file "examples/actors/simple_graph.yaml"
When I validate the actor schema from file
Then the actor schema validation should succeed
And the actor config type should be "graph"
Scenario: Load graph_workflow.yaml example
Given the actor YAML file "examples/actors/graph_workflow.yaml"
When I validate the actor schema from file
Then the actor schema validation should succeed
And the actor config type should be "graph"
# ────────────────────────────────────────────────────────────
# Invalid Actor Name scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject actor without namespace
Given an actor YAML string with name "simple_actor"
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "namespaced"
Scenario: Reject actor with multiple slashes
Given an actor YAML string with name "namespace/sub/actor"
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "namespaced"
Scenario: Reject actor with empty namespace
Given an actor YAML string with name "/actor"
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "namespaced"
Scenario: Reject actor with empty name
Given an actor YAML string with name "namespace/"
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "namespaced"
# ────────────────────────────────────────────────────────────
# Invalid LLM Actor scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject LLM actor without model
Given an actor YAML string with LLM type but no model
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "require 'model'"
# ────────────────────────────────────────────────────────────
# Invalid TOOL Actor scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject TOOL actor without tools
Given an actor YAML string with TOOL type but no tools
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "require at least one tool"
Scenario: Reject TOOL actor with empty tools list
Given an actor YAML string with TOOL type and empty tools
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "require at least one tool"
# ────────────────────────────────────────────────────────────
# Invalid GRAPH Actor scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject GRAPH actor without model
Given an actor YAML string with GRAPH type but no model
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "require 'model'"
Scenario: Reject GRAPH actor without route
Given an actor YAML string with GRAPH type but no route
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "require 'route'"
# ────────────────────────────────────────────────────────────
# Graph Topology Validation scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject graph with duplicate node IDs
Given an actor YAML string with duplicate node IDs
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "Duplicate node IDs"
Scenario: Reject graph with invalid entry node
Given an actor YAML string with non-existent entry node
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "Entry node"
And the validation error should contain "not found"
Scenario: Reject graph with invalid exit node
Given an actor YAML string with non-existent exit node
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "Exit node"
And the validation error should contain "not found"
Scenario: Reject graph with invalid edge from_node
Given an actor YAML string with invalid edge from_node
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "from_node"
And the validation error should contain "not found"
Scenario: Reject graph with invalid edge to_node
Given an actor YAML string with invalid edge to_node
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "to_node"
And the validation error should contain "not found"
Scenario: Reject graph with cycles
Given an actor YAML string with cyclic graph
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "cycle"
# ────────────────────────────────────────────────────────────
# Tool Definition Validation scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject inline tool without namespace
Given an actor YAML string with inline tool name "count_lines"
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "namespaced"
Scenario: Reject inline tool with invalid parameter name
Given an actor YAML string with invalid tool parameter name
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "valid Python identifier"
# ────────────────────────────────────────────────────────────
# Node Definition Validation scenarios
# ────────────────────────────────────────────────────────────
Scenario: Reject node with invalid ID format
Given an actor YAML string with node ID containing spaces
When I validate the actor schema
Then the actor schema validation should fail
And the validation error should contain "alphanumeric"
Scenario: Accept node with underscores and hyphens
Given an actor YAML string with node ID "node_1-test"
When I validate the actor schema
Then the actor schema validation should succeed
# ────────────────────────────────────────────────────────────
# Context View scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load actor with strategist context view
Given an actor YAML string with context_view "strategist"
When I validate the actor schema
Then the actor schema validation should succeed
And the actor context_view should be "strategist"
Scenario: Load actor with executor context view
Given an actor YAML string with context_view "executor"
When I validate the actor schema
Then the actor schema validation should succeed
And the actor context_view should be "executor"
Scenario: Load actor with reviewer context view
Given an actor YAML string with context_view "reviewer"
When I validate the actor schema
Then the actor schema validation should succeed
And the actor context_view should be "reviewer"
Scenario: Load actor with full context view
Given an actor YAML string with context_view "full"
When I validate the actor schema
Then the actor schema validation should succeed
And the actor context_view should be "full"
# ────────────────────────────────────────────────────────────
# Environment Variables scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load actor with environment variables
Given an actor YAML string with env_vars
When I validate the actor schema
Then the actor schema validation should succeed
And the actor should have 2 env_vars
# ────────────────────────────────────────────────────────────
# YAML I/O scenarios
# ────────────────────────────────────────────────────────────
Scenario: Save and reload actor configuration
Given a valid actor configuration object
When I save the actor to YAML file
And I reload the actor from YAML file
Then the reloaded actor should match the original
Scenario: Handle missing YAML file
Given a non-existent actor YAML file path
When I attempt to load the actor from file
Then a FileNotFoundError should be raised
# ────────────────────────────────────────────────────────────
# Edge Priority scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load graph with edge priorities
Given an actor YAML string with edges having different priorities
When I validate the actor schema
Then the actor schema validation should succeed
And the highest priority edge should be 10
# ────────────────────────────────────────────────────────────
# Memory Configuration scenarios
# ────────────────────────────────────────────────────────────
Scenario: Load actor with memory disabled
Given an actor YAML string with memory enabled false
When I validate the actor schema
Then the actor schema validation should succeed
And the actor memory enabled should be false
Scenario: Load actor with message limit
Given an actor YAML string with max_messages 100
When I validate the actor schema
Then the actor schema validation should succeed
And the actor memory max_messages should be 100
Scenario: Load actor with token limit
Given an actor YAML string with max_tokens 8000
When I validate the actor schema
Then the actor schema validation should succeed
And the actor memory max_tokens should be 8000
Scenario: Load actor with summarize_old enabled
Given an actor YAML string with summarize_old true
When I validate the actor schema
Then the actor schema validation should succeed
And the actor memory summarize_old should be true
File diff suppressed because it is too large Load Diff
+74 -1
View File
@@ -980,6 +980,75 @@ The following work from the previous implementation has been completed and will
- benchmark: success (all benchmarks pass)
- **Commit**: `122af46` on `feature/q0-min-coverage``fix(tests): resolve unit test and benchmark failures`
**2026-02-17**: Task C1.schema Complete (Aditya) - Actor YAML Schema Models
- **Implementation**: Created complete actor YAML schema system in `src/cleveragents/actor/schema.py` (695 lines)
- Three actor types with clear separation: LLM (single LLM call + tools), TOOL (tool collections), GRAPH (multi-node workflows)
- Strict name validation enforcing `namespace/name` format (ADR-002 compliance)
- Comprehensive graph topology validation with cycle detection using DFS algorithm
- Type-specific field requirements enforced via Pydantic `model_validator`
- Environment variable interpolation support (`${ENV_VAR}` syntax)
- YAML I/O methods: `from_yaml_file()`, `to_yaml_file()`, `from_yaml()`
- Core models: `ActorType`, `NodeType`, `ContextView` enums; `ToolParameter`, `ToolDefinition`, `MemoryConfig`, `ContextConfigSchema`, `EdgeDefinition`, `NodeDefinition`, `RouteDefinition`, `ActorConfigSchema` Pydantic models
- **Documentation**: Comprehensive reference docs in `docs/reference/actors_schema.md` (420 lines)
- Actor type definitions, field semantics, tool node behavior
- Graph constraints: unique node IDs, entry/exit validation, cycle detection
- Validation rules with examples for valid/invalid configurations
- Error message catalog
- **Examples**: 5 YAML files in `examples/actors/`
- `simple_llm.yaml` - Minimal LLM actor (basic code review)
- `llm_with_tools.yaml` - LLM with tools, memory, context config
- `tool_collection.yaml` - TOOL actor with multiple tool references
- `simple_graph.yaml` - Basic 3-node linear graph workflow
- `graph_workflow.yaml` - Complex graph with conditionals, subgraphs, retry logic
- **Tests**: Comprehensive coverage across all test types
- **Behave**: `features/actor_schema.feature` (50+ scenarios, 300+ steps)
- Valid/invalid configurations for all three actor types
- Name validation (namespace requirement, invalid characters)
- Graph topology (unique nodes, entry/exit validation, cycle detection)
- Tool/node validation, YAML I/O round-trip, error messages
- **Robot**: `robot/actor_schema.robot` (11 test cases)
- Smoke tests for all 5 example YAMLs
- Invalid configuration rejection (missing fields, bad topology, duplicate nodes)
- **ASV**: `benchmarks/actor_schema_bench.py` (8 benchmark classes)
- Parsing/validation for minimal/full LLM, tool, graph actors
- Graph topology validation performance (cycle detection)
- File I/O operations, serialization (`model_dump`)
- **Branch**: `feature/actor-c1-schema` (11 commits across 10 sub-parts)
- **Commit messages follow Conventional Changelog format**:
1. `feat(actor): add core enums for actor schema`
2. `feat(actor): add tool and config pydantic models`
3. `feat(actor): add graph topology models with validation`
4. `feat(actor): add main actor schema and YAML I/O`
5. `docs(actor): add comprehensive actor YAML examples`
6. `docs(actor): add actor schema reference documentation`
7. `test(actor): add behave scenarios for actor schema`
8. `test(actor): add behave step definitions for actor schema`
9. `test(actor): add robot framework integration tests for actor schema`
10. `perf(actor): add asv benchmarks for actor schema`
11. `docs(actor): add c1.schema implementation notes and traceability`
- **Quality metrics**: All nox sessions pass
- lint: 0 findings (pre-existing issues in other files noted)
- typecheck: 0 errors
- security_scan: 0 findings
- unit_tests: All scenarios pass
- integration_tests: All tests pass
- benchmark: All benchmarks execute successfully
- **Implementation Notes section added** to task C1.schema in Implementation Checklist (line 2968)
- Full traceability with file paths and line numbers
- Design decisions documented with rationale
- Test coverage strategy explained
- Dependencies and open questions captured
- **Next steps**: C2.loader (Actor Registry and Loader) depends on this schema foundation
**2026-02-17**: C0.skill.cli Complete (Aditya) - Skill CLI Commands + Output Formatting
- Created `src/cleveragents/application/services/skill_service.py``SkillService` with dual-mode storage pattern (in-memory dict fallback, ready for `UnitOfWork` persistence when `C0.skill.registry` lands):
@@ -1021,7 +1090,6 @@ The following work from the previous implementation has been completed and will
- ASV benchmarks: 4/4 suites pass
- Key decision: Followed `PlanLifecycleService` dual-mode pattern — `SkillService` works fully in-memory now; when Luis adds `C0.skill.registry` with `UnitOfWork`/`SkillRepository`, DB writes are added alongside the in-memory dict without changing the public API
- Key decision: Module-level `_service` singleton (not DI container) matches the pattern in `action.py` CLI; `_reset_skill_service()` enables test isolation
---
## Roadmap
@@ -1496,6 +1564,11 @@ No standalone Q0-Advanced commits planned. Advanced QA enhancements are bundled
### Section 2B: Commit Traceability Fixups [IMMEDIATE]
- [ ] Traceability [Aditya]: Find the actual commit message used for "docs(skill): add skill YAML schema and examples" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [ ] Traceability [Aditya]: Find the actual commit message used for "feat(cli): add skill commands" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [X] Traceability [Aditya]: Found commit "docs(actor): update schema.py module docstring" (2026-02-09T20:23:33+05:30); updated C3.schema Done/commit message to match.
- [ ] Traceability [Aditya]: Find the actual commit message used for "feat(actor): add built-in provider actors" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [ ] Traceability [Aditya]: Find the actual commit message used for "docs(tool): add MCP config examples" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [ ] Traceability [Jeff]: Find the actual commit message used for "feat(actor): compile actor YAML to runtime graphs" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [ ] Traceability [Jeff]: Find the actual commit message used for "feat(tool): add MCP adapter runtime" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
- [ ] Traceability [Luis]: Find the actual commit message used for "feat(skill): add skill registry persistence" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day <n>, <date> <time>` and align the commit message text.
+119
View File
@@ -0,0 +1,119 @@
*** Settings ***
Documentation Smoke tests for Actor YAML schema validation
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Variables ***
${HELPER} ${CURDIR}/helper_actor_schema.py
*** Test Cases ***
Validate Simple LLM Actor YAML
[Documentation] Parse the simple_llm example actor YAML and assert success
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} validate ${CURDIR}/../examples/actors/simple_llm.yaml cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-ok
Validate LLM With Tools Actor YAML
[Documentation] Parse the llm_with_tools example actor YAML and assert success
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} validate ${CURDIR}/../examples/actors/llm_with_tools.yaml cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-ok
Validate Tool Collection Actor YAML
[Documentation] Parse the tool_collection example actor YAML and assert success
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} validate ${CURDIR}/../examples/actors/tool_collection.yaml cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-ok
Validate Simple Graph Actor YAML
[Documentation] Parse the simple_graph example actor YAML and assert success
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} validate ${CURDIR}/../examples/actors/simple_graph.yaml cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-ok
Validate Complex Graph Workflow Actor YAML
[Documentation] Parse the graph_workflow example actor YAML and assert success
[Tags] slow
${result}= Run Process ${PYTHON} ${HELPER} validate ${CURDIR}/../examples/actors/graph_workflow.yaml cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-ok
Reject Actor YAML Without Namespace
[Documentation] Attempt to parse actor YAML with invalid name (no namespace)
[Tags] slow
${invalid_yaml}= Set Variable ${TEMPDIR}${/}invalid_actor_no_namespace.yaml
Create File ${invalid_yaml} name: simple_actor\ntype: llm\ndescription: Test\nmodel: gpt-4\n
${result}= Run Process ${PYTHON} ${HELPER} validate-invalid ${invalid_yaml} cwd=${WORKSPACE}
Log ${result.stdout}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-expected-fail
Should Contain ${result.stdout} namespaced
Reject LLM Actor Without Model
[Documentation] Attempt to parse LLM actor without required model field
[Tags] slow
${invalid_yaml}= Set Variable ${TEMPDIR}${/}invalid_llm_no_model.yaml
Create File ${invalid_yaml} name: test/actor\ntype: llm\ndescription: Missing model\n
${result}= Run Process ${PYTHON} ${HELPER} validate-invalid ${invalid_yaml} cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-expected-fail
Should Contain ${result.stdout} model
Reject TOOL Actor Without Tools
[Documentation] Attempt to parse TOOL actor without tools list
[Tags] slow
${invalid_yaml}= Set Variable ${TEMPDIR}${/}invalid_tool_no_tools.yaml
Create File ${invalid_yaml} name: test/tools\ntype: tool\ndescription: Missing tools\n
${result}= Run Process ${PYTHON} ${HELPER} validate-invalid ${invalid_yaml} cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-expected-fail
Should Contain ${result.stdout} tool
Reject GRAPH Actor Without Route
[Documentation] Attempt to parse GRAPH actor without route definition
[Tags] slow
${invalid_yaml}= Set Variable ${TEMPDIR}${/}invalid_graph_no_route.yaml
Create File ${invalid_yaml} name: test/workflow\ntype: graph\ndescription: Missing route\nmodel: gpt-4\n
${result}= Run Process ${PYTHON} ${HELPER} validate-invalid ${invalid_yaml} cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-expected-fail
Should Contain ${result.stdout} route
Reject Graph With Duplicate Node IDs
[Documentation] Attempt to parse graph with duplicate node IDs
[Tags] slow
${invalid_yaml}= Set Variable ${TEMPDIR}${/}invalid_graph_duplicate_nodes.yaml
${yaml_content}= Catenate SEPARATOR=\n
... name: test/duplicate
... type: graph
... description: Duplicate nodes
... model: gpt-4
... route:
... ${SPACE}${SPACE}nodes:
... ${SPACE}${SPACE}${SPACE}${SPACE}- id: node1
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}type: agent
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}name: First
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}description: First node
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}config:
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}prompt: Test
... ${SPACE}${SPACE}${SPACE}${SPACE}- id: node1
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}type: agent
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}name: Duplicate
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}description: Duplicate ID
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}config:
... ${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}${SPACE}prompt: Test
... ${SPACE}${SPACE}edges: []
... ${SPACE}${SPACE}entry_node: node1
... ${SPACE}${SPACE}exit_nodes: [node1]
Create File ${invalid_yaml} ${yaml_content}
${result}= Run Process ${PYTHON} ${HELPER} validate-invalid ${invalid_yaml} cwd=${WORKSPACE}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} actor-schema-expected-fail
Should Contain ${result.stdout} Duplicate
+56
View File
@@ -0,0 +1,56 @@
"""Robot Framework helper for actor YAML schema validation.
Provides a CLI-style interface for Robot to invoke schema validation
on actor YAML files. Exit code 0 = success, 1 = failure.
Usage:
python robot/helper_actor_schema.py validate <yaml_file>
python robot/helper_actor_schema.py validate-invalid <yaml_file>
"""
from __future__ import annotations
import sys
from pathlib import Path
# Ensure the src directory is on the import path.
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.actor.schema import ActorConfigSchema # noqa: E402
def main() -> int:
"""Entry point called by Robot Framework ``Run Process``."""
if len(sys.argv) < 3:
print("Usage: helper_actor_schema.py <validate|validate-invalid> <file>")
return 1
command = sys.argv[1]
yaml_path = sys.argv[2]
if command == "validate":
try:
config = ActorConfigSchema.from_yaml_file(yaml_path)
print(f"actor-schema-ok: {config.name}")
return 0
except Exception as exc:
print(f"actor-schema-fail: {exc}")
return 1
if command == "validate-invalid":
try:
ActorConfigSchema.from_yaml_file(yaml_path)
print("actor-schema-unexpected-success")
return 1
except Exception as exc:
print(f"actor-schema-expected-fail: {exc}")
return 0
print(f"Unknown command: {command}")
return 1
if __name__ == "__main__":
sys.exit(main())
+711
View File
@@ -0,0 +1,711 @@
"""
Actor YAML Schema Definitions for CleverAgents v3.
This module defines the Pydantic models and enums that validate actor configuration
files. Actors are the v3 replacement for v2's complex reactive stream configurations,
providing a simpler, more declarative way to define AI agent behaviors.
Core Components:
Enums:
- ActorType: LLM, TOOL, or GRAPH execution models
- NodeType: AGENT, TOOL, CONDITIONAL, or SUBGRAPH node types
- ContextView: STRATEGIST, EXECUTOR, REVIEWER, or FULL context filtering
Tool Models:
- ToolParameter: Parameter definitions for inline tools
- ToolDefinition: Complete inline tool with Python code
Graph Models:
- EdgeDefinition: Connections between nodes with conditional routing
- NodeDefinition: Node specifications (agent, tool, conditional, subgraph)
- RouteDefinition: Complete graph topology with validation
Configuration:
- MemoryConfig: Conversation history settings
- ContextConfigSchema: File inclusion and context window configuration
- ActorConfigSchema: Main schema bringing all components together
Usage:
>>> from cleveragents.actor.schema import ActorConfigSchema
>>> config = ActorConfigSchema.from_yaml_file("path/to/actor.yaml")
>>> print(config.name, config.type)
Author: CleverAgents Team
Version: 3.0.0
"""
from __future__ import annotations
from enum import StrEnum
from pathlib import Path
from typing import Any
import yaml
from pydantic import BaseModel, Field, field_validator, model_validator
class ActorType(StrEnum):
"""
Type of actor determining execution behavior.
Actors in v3 are compiled to LangGraph workflows. The type determines
how the actor is compiled and executed:
- LLM: Single LLM call with optional tools (simplest)
- TOOL: Collection of callable tools with agent orchestration
- GRAPH: Multi-node StateGraph with conditional routing (most flexible)
Examples:
>>> actor_config = {"type": ActorType.LLM, "model": "gpt-4"}
>>> if actor_config["type"] == ActorType.GRAPH:
... # Compile to multi-node graph
"""
LLM = "llm" # Single LLM agent with system prompt and optional tools
TOOL = "tool" # Collection of callable tools (file ops, API calls, etc.)
GRAPH = "graph" # Multi-node StateGraph with routing and conditionals
class NodeType(StrEnum):
"""
Type of node in a graph actor.
Graph actors (ActorType.GRAPH) are composed of multiple nodes connected
by edges. Each node performs a specific operation in the workflow:
- AGENT: Invokes an LLM to make decisions or generate content
- TOOL: Executes a specific tool (file operations, API calls, etc.)
- CONDITIONAL: Routes execution based on conditions (if/else logic)
- SUBGRAPH: Embeds another actor as a nested workflow (composition)
Node types enable complex workflows like:
code_writer (AGENT) run_tests (TOOL) check_passed (CONDITIONAL)
if passed: style_check (SUBGRAPH), else: back to code_writer
Examples:
>>> node_def = {"type": NodeType.AGENT, "prompt": "Write Python code"}
>>> if node_def["type"] == NodeType.SUBGRAPH:
... # Load and compile referenced actor
"""
AGENT = "agent" # LLM agent node (makes AI-powered decisions)
TOOL = "tool" # Tool execution node (runs specific function)
CONDITIONAL = "conditional" # Routing node (evaluates conditions)
SUBGRAPH = "subgraph" # Nested actor reference (hierarchical composition)
class ContextView(StrEnum):
"""
Role-based context filtering for actors.
Different actor roles need different levels of context. Providing too much
context wastes tokens and degrades performance; too little causes errors.
ContextView allows actors to request only the information they need.
- STRATEGIST: High-level planning view (project structure, goals, constraints)
- EXECUTOR: Implementation view (code, files, specific tasks)
- REVIEWER: Validation view (changes, diffs, test results)
- FULL: Complete view (all available context, use sparingly)
Examples:
>>> strategist = {"context_view": ContextView.STRATEGIST}
>>> executor = {"context_view": ContextView.EXECUTOR}
>>> # Strategist sees project-level info, executor sees file-level details
"""
STRATEGIST = "strategist" # Planning and strategy context
EXECUTOR = "executor" # Implementation and execution context
REVIEWER = "reviewer" # Review and validation context
FULL = "full" # Complete context (use sparingly)
# ============================================================================
# Tool Models (for inline tool definitions in actors)
# ============================================================================
class ToolParameter(BaseModel):
"""
Parameter definition for inline tool functions.
Used in ToolDefinition to specify input parameters for Python code tools.
Supports type hints and default values for tool inputs.
Attributes:
name: Parameter name (must be valid Python identifier)
type: Python type annotation as string (e.g., "str", "int", "list[str]")
description: Human-readable parameter description
required: Whether parameter must be provided (default: True)
default: Default value if not provided (only for optional params)
Examples:
>>> param = ToolParameter(
... name="input_file",
... type="str",
... description="Path to input file",
... required=True
... )
"""
name: str = Field(..., description="Parameter name")
type: str = Field(..., description="Python type annotation")
description: str = Field(..., description="Parameter description")
required: bool = Field(default=True, description="Whether required")
default: Any | None = Field(default=None, description="Default value")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Ensure parameter name is a valid Python identifier."""
if not v.isidentifier():
msg = f"Parameter name must be valid Python identifier: {v}"
raise ValueError(msg)
return v
class ToolDefinition(BaseModel):
"""
Inline tool definition with Python code.
Allows defining simple tools directly in actor YAML files without
creating separate tool modules. Useful for actor-specific utilities.
Attributes:
name: Tool name (namespaced format: "namespace/tool_name")
description: What the tool does (used in LLM tool selection)
parameters: List of input parameters
code: Python code implementing the tool (must define a function)
Examples:
>>> tool = ToolDefinition(
... name="utils/count_lines",
... description="Count lines in a file",
... parameters=[
... ToolParameter(name="file_path", type="str", description="File")
... ],
... code="def count_lines(file_path: str) -> int:\\n ..."
... )
"""
name: str = Field(..., description="Tool name (namespaced)")
description: str = Field(..., description="Tool description")
parameters: list[ToolParameter] = Field(
default_factory=list, description="Tool parameters"
)
code: str = Field(..., description="Python code for tool")
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Ensure tool name follows namespace/name format."""
if "/" not in v:
msg = f"Tool name must be namespaced (namespace/name): {v}"
raise ValueError(msg)
return v
# ============================================================================
# Configuration Models (memory and context settings)
# ============================================================================
class MemoryConfig(BaseModel):
"""
Conversation history and memory settings for actors.
Controls how much conversation history is retained and passed to the LLM.
Balances context quality with token usage.
Attributes:
enabled: Whether to maintain conversation history (default: True)
max_messages: Maximum messages to retain (None = unlimited)
max_tokens: Maximum tokens in history (None = unlimited)
summarize_old: Whether to summarize old messages (default: False)
Examples:
>>> memory = MemoryConfig(
... enabled=True,
... max_messages=50,
... max_tokens=4000
... )
"""
enabled: bool = Field(default=True, description="Enable conversation memory")
max_messages: int | None = Field(default=None, description="Max messages to retain")
max_tokens: int | None = Field(default=None, description="Max tokens in history")
summarize_old: bool = Field(default=False, description="Summarize old messages")
class ContextConfigSchema(BaseModel):
"""
File inclusion and context window configuration.
Defines which files/directories to include in actor context and how
to manage the context window size.
Attributes:
include_files: List of file paths to include in context
include_dirs: List of directory paths to include in context
exclude_patterns: Glob patterns to exclude from context
max_context_tokens: Maximum context window size (None = model default)
Examples:
>>> context = ContextConfigSchema(
... include_files=["README.md", "src/main.py"],
... include_dirs=["src/", "tests/"],
... exclude_patterns=["**/__pycache__/**", "*.pyc"],
... max_context_tokens=8000
... )
"""
include_files: list[str] = Field(
default_factory=list, description="Files to include"
)
include_dirs: list[str] = Field(
default_factory=list, description="Directories to include"
)
exclude_patterns: list[str] = Field(
default_factory=list, description="Exclusion patterns"
)
max_context_tokens: int | None = Field(
default=None, description="Max context tokens"
)
# ============================================================================
# Graph Models (for ActorType.GRAPH - multi-node workflows)
# ============================================================================
class EdgeDefinition(BaseModel):
"""
Edge connection between nodes in a graph actor.
Defines transitions between nodes with optional conditional routing.
Edges determine the flow of execution through the graph.
Attributes:
from_node: Source node ID
to_node: Target node ID
condition: Optional Python expression for conditional routing
priority: Edge priority for multiple outgoing edges (higher = first)
Examples:
>>> # Simple unconditional edge
>>> edge = EdgeDefinition(from_node="agent1", to_node="tool1")
>>>
>>> # Conditional edge with routing logic
>>> edge = EdgeDefinition(
... from_node="checker",
... to_node="fix_errors",
... condition="state.get('has_errors') == True",
... priority=10
... )
"""
from_node: str = Field(..., description="Source node ID")
to_node: str = Field(..., description="Target node ID")
condition: str | None = Field(
default=None, description="Conditional routing expression"
)
priority: int = Field(default=0, description="Edge priority")
class NodeDefinition(BaseModel):
"""
Node definition in a graph actor.
Represents a single step in a multi-node workflow. Nodes can be
agents (LLM calls), tools (function execution), conditionals (routing),
or subgraphs (nested actors).
Attributes:
id: Unique node identifier within the graph
type: Node type (AGENT, TOOL, CONDITIONAL, SUBGRAPH)
name: Human-readable node name
description: Node purpose and behavior
config: Type-specific configuration (depends on node type)
Node Type Configurations:
AGENT: {"model": "gpt-4", "prompt": "...", "tools": [...]}
TOOL: {"tool_name": "namespace/tool", "parameters": {...}}
CONDITIONAL: {"conditions": [{"check": "...", "route_to": "..."}]}
SUBGRAPH: {"actor_path": "path/to/actor.yaml"}
Examples:
>>> # Agent node
>>> agent = NodeDefinition(
... id="code_writer",
... type=NodeType.AGENT,
... name="Code Writer",
... description="Writes Python code",
... config={"model": "gpt-4", "prompt": "Write clean Python code"}
... )
>>>
>>> # Tool node
>>> tool = NodeDefinition(
... id="run_tests",
... type=NodeType.TOOL,
... name="Test Runner",
... description="Executes pytest",
... config={"tool_name": "testing/run_pytest"}
... )
"""
id: str = Field(..., description="Node ID")
type: NodeType = Field(..., description="Node type")
name: str = Field(..., description="Node name")
description: str = Field(..., description="Node description")
config: dict[str, Any] = Field(default_factory=dict, description="Node config")
@field_validator("id")
@classmethod
def validate_id(cls, v: str) -> str:
"""Ensure node ID is a valid identifier."""
if not v.replace("_", "").replace("-", "").isalnum():
msg = f"Node ID must be alphanumeric with underscores/hyphens: {v}"
raise ValueError(msg)
return v
class RouteDefinition(BaseModel):
"""
Complete graph topology for ActorType.GRAPH actors.
Defines the full multi-node workflow including all nodes, edges,
and entry/exit points. Includes validation for cycle detection
and reachability.
Attributes:
nodes: List of all nodes in the graph
edges: List of all edges connecting nodes
entry_node: ID of the starting node
exit_nodes: List of IDs for terminal nodes
Validation:
- All nodes must have unique IDs
- Entry node must exist in nodes
- All exit nodes must exist in nodes
- All edge references must point to valid nodes
- Graph must be acyclic (no cycles allowed)
- All nodes must be reachable from entry_node
Examples:
>>> route = RouteDefinition(
... nodes=[
... NodeDefinition(
... id="start",
... type=NodeType.AGENT,
... name="Planner",
... description="Plans tasks",
... config={"model": "gpt-4"}
... ),
... NodeDefinition(
... id="execute",
... type=NodeType.TOOL,
... name="Executor",
... description="Runs tasks",
... config={"tool_name": "exec/run"}
... )
... ],
... edges=[
... EdgeDefinition(from_node="start", to_node="execute")
... ],
... entry_node="start",
... exit_nodes=["execute"]
... )
"""
nodes: list[NodeDefinition] = Field(..., description="Graph nodes")
edges: list[EdgeDefinition] = Field(..., description="Graph edges")
entry_node: str = Field(..., description="Entry node ID")
exit_nodes: list[str] = Field(..., description="Exit node IDs")
@field_validator("nodes")
@classmethod
def validate_unique_ids(cls, v: list[NodeDefinition]) -> list[NodeDefinition]:
"""Ensure all node IDs are unique."""
ids = [node.id for node in v]
if len(ids) != len(set(ids)):
duplicates = [id for id in ids if ids.count(id) > 1]
msg = f"Duplicate node IDs found: {set(duplicates)}"
raise ValueError(msg)
return v
def validate_references(self) -> None:
"""
Validate all node references in edges and entry/exit points.
Raises:
ValueError: If any reference points to non-existent node
"""
node_ids = {node.id for node in self.nodes}
# Validate entry node
if self.entry_node not in node_ids:
msg = f"Entry node '{self.entry_node}' not found in nodes"
raise ValueError(msg)
# Validate exit nodes
for exit_node in self.exit_nodes:
if exit_node not in node_ids:
msg = f"Exit node '{exit_node}' not found in nodes"
raise ValueError(msg)
# Validate edge references
for edge in self.edges:
if edge.from_node not in node_ids:
msg = f"Edge from_node '{edge.from_node}' not found in nodes"
raise ValueError(msg)
if edge.to_node not in node_ids:
msg = f"Edge to_node '{edge.to_node}' not found in nodes"
raise ValueError(msg)
def detect_cycles(self) -> list[str]:
"""
Detect cycles in the graph using DFS.
Returns:
List of node IDs involved in cycles (empty if acyclic)
"""
# Build adjacency list
graph: dict[str, list[str]] = {node.id: [] for node in self.nodes}
for edge in self.edges:
graph[edge.from_node].append(edge.to_node)
# DFS cycle detection
visited: set[str] = set()
rec_stack: set[str] = set()
cycle_nodes: list[str] = []
def dfs(node: str) -> bool:
visited.add(node)
rec_stack.add(node)
for neighbor in graph.get(node, []):
if neighbor not in visited:
if dfs(neighbor):
return True
elif neighbor in rec_stack:
cycle_nodes.append(neighbor)
return True
rec_stack.remove(node)
return False
for node in graph:
if node not in visited and dfs(node):
break
return cycle_nodes
# ============================================================================
# Main Actor Configuration Schema
# ============================================================================
class ActorConfigSchema(BaseModel):
"""
Main actor configuration schema bringing all components together.
This is the top-level model for actor YAML files. Actors are the v3
replacement for v2's reactive streams, providing a simpler declarative
way to define AI agent behaviors.
Attributes:
name: Actor name (namespaced format: "namespace/actor_name")
type: Actor type (LLM, TOOL, or GRAPH)
description: What the actor does
version: Schema version (default: "1.0")
model: LLM model name (required for LLM/GRAPH types)
system_prompt: System prompt for LLM actors
tools: List of tool references or inline definitions
context_view: Role-based context filtering
memory: Memory and conversation history settings
context: File inclusion and context window settings
route: Graph topology (required for GRAPH type)
env_vars: Environment variable mappings
Type-Specific Requirements:
ActorType.LLM: Requires model, optional system_prompt and tools
ActorType.TOOL: Requires tools list
ActorType.GRAPH: Requires model and route
Examples:
>>> # Simple LLM actor
>>> actor = ActorConfigSchema(
... name="assistants/code_reviewer",
... type=ActorType.LLM,
... description="Reviews Python code for best practices",
... model="gpt-4",
... system_prompt="You are an expert Python code reviewer"
... )
>>>
>>> # Graph actor with multiple nodes
>>> actor = ActorConfigSchema(
... name="workflows/test_driven_dev",
... type=ActorType.GRAPH,
... description="Test-driven development workflow",
... model="gpt-4",
... route=RouteDefinition(...)
... )
"""
name: str = Field(..., description="Actor name (namespaced)")
type: ActorType = Field(..., description="Actor type")
description: str = Field(..., description="Actor description")
version: str = Field(default="1.0", description="Schema version")
# LLM configuration
model: str | None = Field(default=None, description="LLM model name")
system_prompt: str | None = Field(default=None, description="System prompt")
# Tool configuration
tools: list[str | ToolDefinition] = Field(
default_factory=list, description="Tool references or definitions"
)
# Context and memory
context_view: ContextView | None = Field(
default=None, description="Context filtering view"
)
memory: MemoryConfig = Field(
default_factory=MemoryConfig, description="Memory settings"
)
context: ContextConfigSchema = Field(
default_factory=ContextConfigSchema, description="Context settings"
)
# Graph configuration
route: RouteDefinition | None = Field(
default=None, description="Graph topology (for GRAPH type)"
)
# Environment variables
env_vars: dict[str, str] = Field(
default_factory=dict, description="Environment variable mappings"
)
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""Ensure actor name follows namespace/name format."""
if "/" not in v:
msg = f"Actor name must be namespaced (namespace/name): {v}"
raise ValueError(msg)
# Check for exactly one slash
parts = v.split("/")
if len(parts) != 2:
msg = (
f"Actor name must be namespaced with exactly one slash "
f"(namespace/name): {v}"
)
raise ValueError(msg)
namespace, name = parts
if not namespace or not name:
msg = (
f"Actor name must be namespaced with non-empty namespace and name: {v}"
)
raise ValueError(msg)
return v
@model_validator(mode="after")
def validate_type_requirements(self) -> ActorConfigSchema:
"""Validate type-specific requirements."""
# LLM actors require model
if self.type == ActorType.LLM and not self.model:
msg = "LLM actors require 'model' field"
raise ValueError(msg)
# TOOL actors require tools
if self.type == ActorType.TOOL and not self.tools:
msg = "TOOL actors require at least one tool"
raise ValueError(msg)
# GRAPH actors require model and route
if self.type == ActorType.GRAPH:
if not self.model:
msg = "GRAPH actors require 'model' field"
raise ValueError(msg)
if not self.route:
msg = "GRAPH actors require 'route' field"
raise ValueError(msg)
# Validate route references and cycles
self.route.validate_references()
cycles = self.route.detect_cycles()
if cycles:
msg = f"Graph contains cycles involving nodes: {cycles}"
raise ValueError(msg)
return self
@classmethod
def from_yaml_file(cls, file_path: str | Path) -> ActorConfigSchema:
"""
Load actor configuration from YAML file.
Args:
file_path: Path to YAML file
Returns:
Parsed and validated ActorConfigSchema
Raises:
FileNotFoundError: If file doesn't exist
yaml.YAMLError: If YAML is invalid
ValidationError: If schema validation fails
Examples:
>>> actor = ActorConfigSchema.from_yaml_file("actors/reviewer.yaml")
>>> print(actor.name, actor.type)
"""
path = Path(file_path)
if not path.exists():
msg = f"Actor file not found: {file_path}"
raise FileNotFoundError(msg)
with path.open("r", encoding="utf-8") as f:
data = yaml.safe_load(f)
return cls.model_validate(data)
def to_yaml_file(self, file_path: str | Path) -> None:
"""
Save actor configuration to YAML file.
Args:
file_path: Path to save YAML file
Examples:
>>> actor.to_yaml_file("actors/new_actor.yaml")
"""
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8") as f:
yaml.safe_dump(
self.model_dump(mode="json", exclude_none=True),
f,
default_flow_style=False,
sort_keys=False,
)
__all__ = [
"ActorConfigSchema",
"ActorType",
"ContextConfigSchema",
"ContextView",
"EdgeDefinition",
"MemoryConfig",
"NodeDefinition",
"NodeType",
"RouteDefinition",
"ToolDefinition",
"ToolParameter",
]