feat(context): add strategy configuration to context policy YAML schema #10669

Merged
HAL9000 merged 5 commits from feat/v3.6.0/context-policy-strategy-config into master 2026-06-19 05:10:25 +00:00
4 changed files with 343 additions and 1 deletions
@@ -0,0 +1,61 @@
@context_policy @strategy_configuration
Feature: Context Policy Strategy Configuration
As a CleverAgents developer
I want to configure context assembly strategies in context policy YAML
So that I can control how context is assembled during ACMS phases
Scenario: Create context policy with basic strategy
Given a context policy with strategy "basic"
When I validate the context policy
Then the policy strategy should be "basic"
And the policy should be valid
Scenario: Create context policy with semantic strategy
Given a context policy with strategy "semantic"
When I validate the context policy
Then the policy strategy should be "semantic"
And the policy should be valid
Scenario: Create context policy with relevance_scoring strategy
Given a context policy with strategy "relevance_scoring"
When I validate the context policy
Then the policy strategy should be "relevance_scoring"
And the policy should be valid
Scenario: Create context policy with adaptive strategy
Given a context policy with strategy "adaptive"
When I validate the context policy
Then the policy strategy should be "adaptive"
And the policy should be valid
Scenario: Create context policy with fusion strategy
Given a context policy with strategy "fusion"
When I validate the context policy
Then the policy strategy should be "fusion"
And the policy should be valid
Scenario: Reject invalid strategy name
Given a context policy with strategy "invalid_strategy"
When I validate the context policy
Then the policy should be invalid
And the strategy error should mention "Invalid strategy"
Scenario: Create context policy with strategy config
Given a context policy with strategy "semantic"
And strategy config with parameter "threshold" set to 0.5
When I validate the context policy
Then the strategy_config should contain "threshold"
And the strategy_config["threshold"] should be 0.5
Scenario: Create context policy without strategy
Given a context policy without strategy
When I validate the context policy
Then the policy strategy should be None
And the policy should be valid
Scenario: Create context policy with strategy but no config
Given a context policy with strategy "basic"
And no strategy config
When I validate the context policy
Then the strategy_config should be None
And the policy should be valid
@@ -0,0 +1,125 @@
"""Step definitions for ProjectContextPolicy strategy configuration tests."""
from __future__ import annotations
from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError
from cleveragents.domain.models.core.context_policy import (
ProjectContextPolicy,
)
# -------------------------------------------------------------------
# Strategy field support
# -------------------------------------------------------------------
@given('a context policy with strategy "{strategy}"')
def step_policy_with_strategy(context: Context, strategy: str) -> None:
context.strategy = strategy
context.strategy_config = None
context.policy_error = None
@given("a context policy without strategy")
def step_policy_without_strategy(context: Context) -> None:
context.strategy = None
context.strategy_config = None
context.policy_error = None
@given('strategy config with parameter "{key}" set to {value}')
def step_add_strategy_config(context: Context, key: str, value: str) -> None:
if context.strategy_config is None:
context.strategy_config = {}
# Parse the value
try:
# Try to parse as float
context.strategy_config[key] = float(value)
except ValueError:
try:
# Try to parse as int
context.strategy_config[key] = int(value)
except ValueError:
# Keep as string
context.strategy_config[key] = value
@given("no strategy config")
def step_no_strategy_config(context: Context) -> None:
context.strategy_config = None
@when("I validate the context policy")
def step_validate_policy(context: Context) -> None:
context.policy_error = None
try:
context.policy = ProjectContextPolicy(
strategy=context.strategy,
strategy_config=context.strategy_config,
)
except ValidationError as exc:
context.policy_error = str(exc)
@then('the policy strategy should be "{strategy}"')
def step_strategy_is(context: Context, strategy: str) -> None:
if strategy == "None":
assert context.policy.strategy is None
else:
assert context.policy.strategy == strategy
@then("the policy strategy should be None")
def step_strategy_is_none(context: Context) -> None:
assert context.policy.strategy is None
@then("the policy should be valid")
def step_policy_valid(context: Context) -> None:
assert context.policy_error is None, (
f"Expected valid policy but got error: {context.policy_error}"
)
@then("the policy should be invalid")
def step_policy_invalid(context: Context) -> None:
assert context.policy_error is not None, "Expected invalid policy but it was valid"
@then('the strategy error should mention "{text}"')
def step_error_mentions(context: Context, text: str) -> None:
assert text in context.policy_error, (
f"Expected '{text}' in error: {context.policy_error}"
)
# -------------------------------------------------------------------
# Strategy configuration parameters
# -------------------------------------------------------------------
@then('the strategy_config should contain "{key}"')
def step_strategy_config_contains(context: Context, key: str) -> None:
assert context.policy.strategy_config is not None
assert key in context.policy.strategy_config
@then('the strategy_config["{key}"] should be {value}')
def step_strategy_config_value(context: Context, key: str, value: str) -> None:
assert context.policy.strategy_config is not None
# Parse the expected value
try:
expected = float(value)
except ValueError:
try:
expected = int(value)
except ValueError:
expected = value
assert context.policy.strategy_config[key] == expected
@then("the strategy_config should be None")
def step_strategy_config_none(context: Context) -> None:
assert context.policy.strategy_config is None
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Update context_policy.py with strategy configuration support."""
import sys
from pathlib import Path
# Read the file
policy_file = Path("src/cleveragents/domain/models/core/context_policy.py")
content = policy_file.read_text()
# Update the imports to include Any
content = content.replace(
"from typing import TYPE_CHECKING", "from typing import TYPE_CHECKING, Any"
)
# Add VALID_STRATEGIES constant after VALID_PHASES
old_phases = (
"VALID_PHASES: frozenset[str] = frozenset("
'{"default", "strategize", "execute", "apply"})'
)
new_phases = (
"VALID_PHASES: frozenset[str] = frozenset("
'{"default", "strategize", "execute", "apply"})\n\n'
"VALID_STRATEGIES: frozenset[str] = frozenset({\n"
' "basic",\n'
' "semantic",\n'
' "relevance_scoring",\n'
' "adaptive",\n'
' "fusion",\n'
"})"
)
content = content.replace(old_phases, new_phases)
# Update the docstring for ProjectContextPolicy to mention strategy
old_docstring = (
"class ProjectContextPolicy(BaseModel):\n"
' """Controls what context is available during each ACMS phase.\n'
"\n"
" Uses view inheritance: ``default`` → ``strategize`` →\n"
" ``execute`` → ``apply``. Each phase can override or inherit\n"
" from its parent.\n"
"\n"
" An empty ``ProjectContextPolicy()`` defaults to including\n"
" everything (the ``default_view`` has empty include lists which\n"
' means "all").\n'
' """'
)
new_docstring = (
"class ProjectContextPolicy(BaseModel):\n"
' """Controls what context is available during each ACMS phase.\n'
"\n"
" Uses view inheritance: ``default`` → ``strategize`` →\n"
" ``execute`` → ``apply``. Each phase can override or inherit\n"
" from its parent.\n"
"\n"
" An empty ``ProjectContextPolicy()`` defaults to including\n"
" everything (the ``default_view`` has empty include lists which\n"
' means "all").\n'
"\n"
" Optionally specifies a context assembly strategy and its\n"
" configuration parameters.\n"
' """'
)
content = content.replace(old_docstring, new_docstring)
# Add strategy and strategy_config fields before the resolve_view method
old_fields = (
" apply_view: ContextView | None = Field(\n"
" default=None,\n"
' description=("Overrides for Apply (inherits from execute if None)"),\n'
" )\n"
"\n"
" def resolve_view(self, phase: str) -> ContextView:"
)
error_msg = "f\"Invalid strategy '{v}': must be one of {sorted(VALID_STRATEGIES)}\""
new_fields = (
" apply_view: ContextView | None = Field(\n"
" default=None,\n"
' description=("Overrides for Apply (inherits from execute if None)"),\n'
" )\n"
" strategy: str | None = Field(\n"
" default=None,\n"
" description=(\n"
' "Context assembly strategy name. "\n'
' "Valid values: basic, semantic, relevance_scoring, adaptive, fusion"\n'
" ),\n"
" )\n"
" strategy_config: dict[str, Any] | None = Field(\n"
" default=None,\n"
' description="Strategy-specific configuration parameters",\n'
" )\n"
"\n"
' @field_validator("strategy")\n'
" @classmethod\n"
" def _validate_strategy(\n"
" cls: type[ProjectContextPolicy],\n"
" v: str | None,\n"
" ) -> str | None:\n"
' """Validate that strategy name is in the list of valid strategies."""\n'
" if v is not None and v not in VALID_STRATEGIES:\n"
" raise ValueError(\n"
" " + error_msg + "\n"
" )\n"
" return v\n"
"\n"
" def resolve_view(self, phase: str) -> ContextView:"
)
content = content.replace(old_fields, new_fields)
# Write the updated content
policy_file.write_text(content)
print("Updated context_policy.py successfully")
sys.exit(0)
@@ -34,7 +34,7 @@ Based on ``docs/specification.md`` Context section and ADR-004.
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
@@ -47,6 +47,16 @@ if TYPE_CHECKING:
VALID_PHASES: frozenset[str] = frozenset({"default", "strategize", "execute", "apply"})
VALID_STRATEGIES: frozenset[str] = frozenset(
{
"basic",
"semantic",
"relevance_scoring",
"adaptive",
"fusion",
}
)
_INHERITANCE_CHAIN: dict[str, list[str]] = {
"default": ["default"],
"strategize": ["strategize", "default"],
@@ -125,6 +135,9 @@ class ProjectContextPolicy(BaseModel):
An empty ``ProjectContextPolicy()`` defaults to including
everything (the ``default_view`` has empty include lists which
means "all").
Optionally specifies a context assembly strategy and its
configuration parameters.
"""
default_view: ContextView = Field(
@@ -143,6 +156,30 @@ class ProjectContextPolicy(BaseModel):
default=None,
description=("Overrides for Apply (inherits from execute if None)"),
)
strategy: str | None = Field(
default=None,
description=(
"Context assembly strategy name. "
"Valid values: basic, semantic, relevance_scoring, adaptive, fusion"
),
)
strategy_config: dict[str, Any] | None = Field(
default=None,
description="Strategy-specific configuration parameters",
)
@field_validator("strategy")
@classmethod
def _validate_strategy(
cls: type[ProjectContextPolicy],
v: str | None,
) -> str | None:
"""Validate that strategy name is in the list of valid strategies."""
if v is not None and v not in VALID_STRATEGIES:
raise ValueError(
f"Invalid strategy '{v}': must be one of {sorted(VALID_STRATEGIES)}"
)
return v
def resolve_view(self, phase: str) -> ContextView:
"""Resolve the effective view for a given phase.