refactor(subplans): Centralize subplan errors per v3.3.0 spec (#8725)
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 1m5s
CI / typecheck (pull_request) Successful in 1m14s
CI / benchmark-regression (pull_request) Failing after 56s
CI / helm (pull_request) Successful in 31s
CI / push-validation (pull_request) Successful in 29s
CI / build (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 1m32s
CI / security (pull_request) Successful in 1m38s
CI / e2e_tests (pull_request) Successful in 3m17s
CI / integration_tests (pull_request) Failing after 3m44s
CI / unit_tests (pull_request) Failing after 4m52s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 4s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Failing after 1m5s
CI / typecheck (pull_request) Successful in 1m14s
CI / benchmark-regression (pull_request) Failing after 56s
CI / helm (pull_request) Successful in 31s
CI / push-validation (pull_request) Successful in 29s
CI / build (pull_request) Successful in 52s
CI / quality (pull_request) Successful in 1m32s
CI / security (pull_request) Successful in 1m38s
CI / e2e_tests (pull_request) Successful in 3m17s
CI / integration_tests (pull_request) Failing after 3m44s
CI / unit_tests (pull_request) Failing after 4m52s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 4s
Move SubplanSpawnError from local subplan_service definition to centralized cleveragents.core.exceptions alongside four new spec-defined error types: SubplanExecutionError, MaxParallelExceededError, and SubplanDepthLimitError. Per the v3.3.0 specification (AUTO-ARCH-6), all subplan-related errors are defined in exceptions.py with proper inheritance hierarchy under DomainError/ PlanError/BusinessRuleViolation. The old local SpawnValidationError class has been replaced with SubplanSpawnError(PlanError) with a simplified constructor API (message string instead of validation_errors list). Updates: - exceptions.py: Added 4 subplan error classes + __all__ entries - subplan_service.py: Remove local SpawnValidationError, import SubplanSpawnError from exceptions - services/__init__.py: Update TYPE_CHECKING stub and _LAZY_IMPORTS for new location - vulture_whitelist.py: Replace old entry with new error class names - docs/reference/subplan_service.md: Update to reference SubplanSpawnError (v3.3.0) - features/*.feature + steps: Update test references from SpawnValidationError to SubplanSpawnError
This commit is contained in:
@@ -89,7 +89,7 @@ Before any child plan is created, `validate_spawn` checks:
|
||||
5. **Correct decision types**: Each entry's decision must be either
|
||||
`subplan_spawn` or `subplan_parallel_spawn`.
|
||||
|
||||
If any check fails, `SpawnValidationError` is raised with all errors.
|
||||
If any check fails, `SubplanSpawnError` is raised with a message listing all errors.
|
||||
|
||||
## Integration with Other Services
|
||||
|
||||
@@ -129,12 +129,13 @@ parent_plan.subplan_statuses = result.spawned_statuses
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | When |
|
||||
|--------------------------|---------------------------------------------|
|
||||
| `ValueError` | `None` or empty required arguments |
|
||||
| `SpawnValidationError` | Validation checks fail (resource scope, |
|
||||
| | merge strategy, max_parallel, action name, |
|
||||
| | or decision type) |
|
||||
| Error | When |
|
||||
|------------------------------|---------------------------------------------|
|
||||
| `ValueError` | `None` or empty required arguments |
|
||||
| `SubplanSpawnError` (v3.3.0) | Validation checks fail (resource scope, |
|
||||
| | merge strategy, max_parallel, action name, |
|
||||
| | or decision type) |
|
||||
|
||||
`SpawnValidationError` inherits from `ValidationError` and includes a
|
||||
`validation_errors` list with detailed messages for each failure.
|
||||
`SubplanSpawnError` inherits from `PlanError` and carries a descriptive message
|
||||
listing all validation failures. The error is centralized in
|
||||
`cleveragents.core.exceptions` (moved from the local subplan_service module in v3.3.0).
|
||||
|
||||
@@ -530,10 +530,10 @@ Feature: Services __init__ lazy-import coverage (round 3)
|
||||
When svcov3 I access lazy attribute "SpawnResult"
|
||||
Then svcov3 the attribute "SpawnResult" should be resolved
|
||||
|
||||
Scenario: svcov3 lazy-load SpawnValidationError
|
||||
Scenario: svcov3 lazy-load SubplanSpawnError
|
||||
Given svcov3 a fresh services module
|
||||
When svcov3 I access lazy attribute "SpawnValidationError"
|
||||
Then svcov3 the attribute "SpawnValidationError" should be resolved
|
||||
When svcov3 I access lazy attribute "SubplanSpawnError"
|
||||
Then svcov3 the attribute "SubplanSpawnError" should be resolved
|
||||
|
||||
Scenario: svcov3 lazy-load SpawnValidationResult
|
||||
Given svcov3 a fresh services module
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""Step definitions for subplan service coverage boost scenarios.
|
||||
|
||||
Targets uncovered lines in subplan_service.py:
|
||||
- Lines 129-131: SpawnValidationError.__init__
|
||||
- Lines 32-35: SubplanSpawnError import from core.exceptions (v3.3.0)
|
||||
- Line 167: decision_service property
|
||||
- Line 207: spawn() config=None guard
|
||||
- Line 209: spawn() spawn_entries=None guard
|
||||
- Line 220: spawn() raising SpawnValidationError
|
||||
- Line 215: spawn() raising SubplanSpawnError
|
||||
- Line 301: validate_spawn() config=None guard
|
||||
- Line 303: validate_spawn() spawn_entries=None guard
|
||||
- Lines 334-336: empty action_name validation
|
||||
@@ -22,10 +22,11 @@ from behave.runner import Context
|
||||
|
||||
from cleveragents.application.services.subplan_service import (
|
||||
SpawnEntry,
|
||||
SpawnValidationError,
|
||||
SpawnValidationResult,
|
||||
SubplanService,
|
||||
)
|
||||
|
||||
from cleveragents.core.exceptions import SubplanSpawnError
|
||||
from cleveragents.domain.models.core.decision import (
|
||||
ContextSnapshot,
|
||||
Decision,
|
||||
@@ -156,11 +157,13 @@ def step_given_entry_whitespace_action(context: Context) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('a SpawnValidationError is created with errors "{err1}" and "{err2}"')
|
||||
def step_when_create_spawn_validation_error(
|
||||
@when('a SubplanSpawnError is created for errors "{err1}" and "{err2}"')
|
||||
def step_when_create_subplan_spawn_error(
|
||||
context: Context, err1: str, err2: str
|
||||
) -> None:
|
||||
context.cov_sve = SpawnValidationError([err1, err2])
|
||||
# SubplanSpawnError (v3.3.0) takes a message string instead of list
|
||||
msg = f"Spawn validation failed: {err1}; {err2}"
|
||||
context.cov_sve = SubplanSpawnError(msg)
|
||||
|
||||
|
||||
@when("the decision_service property is accessed")
|
||||
@@ -205,7 +208,7 @@ def step_when_spawn_validation_fail(context: Context) -> None:
|
||||
config=context.cov_config,
|
||||
spawn_entries=context.cov_spawn_entries,
|
||||
)
|
||||
except SpawnValidationError as exc:
|
||||
except SubplanSpawnError as exc:
|
||||
context.cov_error = exc
|
||||
|
||||
|
||||
@@ -273,14 +276,16 @@ def step_when_build_entries_none(context: Context) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the SpawnValidationError validation_errors list should have {n:d} entries")
|
||||
@then("the SubplanSpawnError message should contain {n:d} semicolons")
|
||||
def step_then_sve_count(context: Context, n: int) -> None:
|
||||
assert len(context.cov_sve.validation_errors) == n, (
|
||||
f"Expected {n} errors, got {len(context.cov_sve.validation_errors)}"
|
||||
# With v3.3.0 API, errors are joined into a single message string
|
||||
msg = str(context.cov_sve)
|
||||
assert msg.count(";") == n, (
|
||||
f"Expected {n} semicolons in message, got {msg.count(';')}"
|
||||
)
|
||||
|
||||
|
||||
@then('the SpawnValidationError message should contain "{text}"')
|
||||
@then('the SubplanSpawnError message should contain "{text}"')
|
||||
def step_then_sve_message(context: Context, text: str) -> None:
|
||||
msg = str(context.cov_sve)
|
||||
assert text in msg, f"Expected '{text}' in message: {msg}"
|
||||
@@ -304,17 +309,17 @@ def step_then_value_error_text(context: Context, text: str) -> None:
|
||||
assert text in str(context.cov_error), f"Expected '{text}' in '{context.cov_error}'"
|
||||
|
||||
|
||||
@then("a SpawnValidationError should be raised")
|
||||
@then("a SubplanSpawnError should be raised")
|
||||
def step_then_sve_raised(context: Context) -> None:
|
||||
assert context.cov_error is not None, (
|
||||
"Expected SpawnValidationError but no error was raised"
|
||||
"Expected SubplanSpawnError but no error was raised"
|
||||
)
|
||||
assert isinstance(context.cov_error, SpawnValidationError), (
|
||||
f"Expected SpawnValidationError, got {type(context.cov_error).__name__}"
|
||||
assert isinstance(context.cov_error, SubplanSpawnError), (
|
||||
f"Expected SubplanSpawnError, got {type(context.cov_error).__name__}"
|
||||
)
|
||||
|
||||
|
||||
@then('the SpawnValidationError should mention "{text}"')
|
||||
@then('the SubplanSpawnError should mention "{text}"')
|
||||
def step_then_sve_mentions(context: Context, text: str) -> None:
|
||||
msg = str(context.cov_error)
|
||||
assert text.lower() in msg.lower(), f"Expected '{text}' in error message: {msg}"
|
||||
|
||||
@@ -9,9 +9,10 @@ from behave.runner import Context
|
||||
|
||||
from cleveragents.application.services.subplan_service import (
|
||||
SpawnEntry,
|
||||
SpawnValidationError,
|
||||
SubplanService,
|
||||
)
|
||||
|
||||
from cleveragents.core.exceptions import SubplanSpawnError
|
||||
from cleveragents.domain.models.core.decision import (
|
||||
ContextSnapshot,
|
||||
Decision,
|
||||
@@ -118,7 +119,7 @@ def step_when_spawn(context: Context) -> None:
|
||||
spawn_entries=context.spawn_entries,
|
||||
available_resources=getattr(context, "available_resources", None),
|
||||
)
|
||||
except (SpawnValidationError, ValueError) as exc:
|
||||
except (SubplanSpawnError, ValueError) as exc:
|
||||
context.spawn_error = exc
|
||||
context.spawn_result = None
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
@phase1 @subplan @coverage
|
||||
Feature: Subplan Service Coverage Boost
|
||||
Additional scenarios targeting uncovered code paths in
|
||||
SubplanService, SpawnValidationError, and related helpers.
|
||||
SubplanService, SubplanSpawnError, and related helpers.
|
||||
|
||||
# --- SpawnValidationError direct instantiation (lines 129-131) ---
|
||||
# --- SubplanSpawnError direct instantiation (v3.3.0) ---
|
||||
|
||||
@error_object
|
||||
Scenario: SpawnValidationError stores validation errors and formats message
|
||||
When a SpawnValidationError is created with errors "bad resource" and "missing merge"
|
||||
Then the SpawnValidationError validation_errors list should have 2 entries
|
||||
And the SpawnValidationError message should contain "bad resource"
|
||||
And the SpawnValidationError message should contain "missing merge"
|
||||
Scenario: SubplanSpawnError message format includes errors
|
||||
When a SubplanSpawnError is created for errors "bad resource" and "missing merge"
|
||||
Then the SubplanSpawnError message should contain {1:d} semicolons
|
||||
And the SubplanSpawnError message should contain "Spawn validation failed"
|
||||
And the SubplanSpawnError message should contain "bad resource"
|
||||
And the SubplanSpawnError message should contain "missing merge"
|
||||
|
||||
# --- decision_service property (line 167) ---
|
||||
|
||||
@@ -39,17 +40,17 @@ Feature: Subplan Service Coverage Boost
|
||||
When spawn is called with spawn_entries set to None
|
||||
Then a ValueError should be raised with text "spawn_entries must not be None"
|
||||
|
||||
# --- spawn() raising SpawnValidationError (line 220) ---
|
||||
# --- spawn() raising SubplanSpawnError (v3.3.0) ---
|
||||
|
||||
@spawn_validation_error
|
||||
Scenario: Spawn raises SpawnValidationError when validation fails
|
||||
Scenario: Spawn raises SubplanSpawnError when validation fails
|
||||
Given a subplan service constructed with a mock decision service
|
||||
And a parent plan for coverage tests
|
||||
And a subplan config with merge strategy forcibly set to None
|
||||
And a spawn entry with a valid spawn decision for coverage tests
|
||||
When spawn is called and validation is expected to fail
|
||||
Then a SpawnValidationError should be raised
|
||||
And the SpawnValidationError should mention "merge strategy"
|
||||
Then a SubplanSpawnError should be raised
|
||||
And the SubplanSpawnError should mention "merge strategy"
|
||||
|
||||
# --- validate_spawn() with config=None (line 301) ---
|
||||
|
||||
|
||||
@@ -312,8 +312,9 @@ if TYPE_CHECKING:
|
||||
from cleveragents.application.services.subplan_service import (
|
||||
SpawnResult as SpawnResult,
|
||||
)
|
||||
from cleveragents.application.services.subplan_service import (
|
||||
SpawnValidationError as SpawnValidationError,
|
||||
# SubplanSpawnError is centralized in exceptions.py (v3.3.0)
|
||||
from cleveragents.core.exceptions import (
|
||||
SubplanSpawnError as SubplanSpawnError,
|
||||
)
|
||||
from cleveragents.application.services.subplan_service import (
|
||||
SpawnValidationResult as SpawnValidationResult,
|
||||
@@ -535,7 +536,8 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"SpawnEntry": ("subplan_service", "SpawnEntry"),
|
||||
"SpawnMetadata": ("subplan_service", "SpawnMetadata"),
|
||||
"SpawnResult": ("subplan_service", "SpawnResult"),
|
||||
"SpawnValidationError": ("subplan_service", "SpawnValidationError"),
|
||||
# SubplanSpawnError relocated to cleveragents.core.exceptions (v3.3.0)
|
||||
"SubplanSpawnError": ("..core/exceptions", "SubplanSpawnError"),
|
||||
"SpawnValidationResult": ("subplan_service", "SpawnValidationResult"),
|
||||
"SubplanService": ("subplan_service", "SubplanService"),
|
||||
"TemporalService": ("temporal_service", "TemporalService"),
|
||||
|
||||
@@ -29,7 +29,10 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from ulid import ULID
|
||||
|
||||
from cleveragents.core.exceptions import ValidationError
|
||||
from cleveragents.core.exceptions import (
|
||||
SubplanSpawnError,
|
||||
ValidationError,
|
||||
)
|
||||
from cleveragents.domain.models.core.decision import Decision, DecisionType
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
ExecutionMode,
|
||||
@@ -122,24 +125,6 @@ class SpawnResult:
|
||||
child_plans: list[Plan] = field(default_factory=list)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exceptions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SpawnValidationError(ValidationError):
|
||||
"""Raised when spawn validation fails.
|
||||
|
||||
Attributes:
|
||||
validation_errors: List of individual validation failure messages.
|
||||
"""
|
||||
|
||||
def __init__(self, validation_errors: list[str]) -> None:
|
||||
self.validation_errors: list[str] = validation_errors
|
||||
errors_str: str = "; ".join(validation_errors)
|
||||
super().__init__(f"Spawn validation failed: {errors_str}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Service
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -208,7 +193,7 @@ class SubplanService:
|
||||
ValueError: If *parent_plan*, *config*, or *spawn_entries* is
|
||||
``None``.
|
||||
ValueError: If *spawn_entries* is empty.
|
||||
SpawnValidationError: If validation fails.
|
||||
SubplanSpawnError: If validation fails.
|
||||
"""
|
||||
if parent_plan is None:
|
||||
raise ValueError("parent_plan must not be None")
|
||||
@@ -226,7 +211,8 @@ class SubplanService:
|
||||
available_resources=available_resources,
|
||||
)
|
||||
if not validation.valid:
|
||||
raise SpawnValidationError(validation.errors)
|
||||
errors_str = "; ".join(validation.errors)
|
||||
raise SubplanSpawnError(f"Spawn validation failed: {errors_str}")
|
||||
|
||||
# Build statuses, metadata, and real child Plan objects
|
||||
parent_id: str = parent_plan.identity.plan_id
|
||||
@@ -481,7 +467,6 @@ __all__: list[str] = [
|
||||
"SpawnEntry",
|
||||
"SpawnMetadata",
|
||||
"SpawnResult",
|
||||
"SpawnValidationError",
|
||||
"SpawnValidationResult",
|
||||
"SubplanService",
|
||||
]
|
||||
|
||||
@@ -359,6 +359,116 @@ class InvariantViolationError(BusinessRuleViolation):
|
||||
self.action_text = action_text
|
||||
|
||||
|
||||
# Subplan Errors (v3.3.0)
|
||||
class SubplanSpawnError(PlanError):
|
||||
"""Raised when subplan spawning fails.
|
||||
|
||||
Covers spawn-level failures including validation errors, upstream
|
||||
service unavailability, and plan state constraints that prevent
|
||||
child plans from being created.
|
||||
|
||||
Attributes:
|
||||
message: Human-readable error description.
|
||||
details: Optional structured context (e.g. plan_id).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize subplan spawn error.
|
||||
|
||||
Args:
|
||||
message: Human-readable description of the failure.
|
||||
details: Optional structured context.
|
||||
"""
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class SubplanExecutionError(DomainError):
|
||||
"""Raised when a subplan execution fails after spawning.
|
||||
|
||||
Covers runtime failures during child-plan execution such as
|
||||
sandbox errors, merge conflicts (when FAIL_ON_CONFLICT), and
|
||||
per-subplan timeouts.
|
||||
|
||||
Attributes:
|
||||
message: Human-readable error description.
|
||||
details: Optional structured context (e.g., subplan_id).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Initialize subplan execution error.
|
||||
|
||||
Args:
|
||||
message: Human-readable description of the failure.
|
||||
details: Optional structured context.
|
||||
"""
|
||||
super().__init__(message, details)
|
||||
|
||||
|
||||
class MaxParallelExceededError(BusinessRuleViolation):
|
||||
"""Raised when concurrent subplans exceed the configured max limit.
|
||||
|
||||
Attributes:
|
||||
plan_id: ULID of the parent plan that triggered the limit.
|
||||
requested: Number of subplans requested to execute concurrently.
|
||||
max_allowed: Configured maximum parallel subplans.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
plan_id: str,
|
||||
requested: int,
|
||||
max_allowed: int,
|
||||
) -> None:
|
||||
"""Initialize max-parallel-exceeded error.
|
||||
|
||||
Args:
|
||||
plan_id: ULID of the parent plan.
|
||||
requested: Number of subplans attempted.
|
||||
max_allowed: The configured ``max_parallel`` value.
|
||||
"""
|
||||
super().__init__(
|
||||
f"Concurrent subplan count {requested} exceeds "
|
||||
f"max_parallel limit {max_allowed} for plan {plan_id}"
|
||||
)
|
||||
self.plan_id = plan_id
|
||||
self.requested = requested
|
||||
self.max_allowed = max_allowed
|
||||
|
||||
|
||||
class SubplanDepthLimitError(BusinessRuleViolation):
|
||||
"""Raised when subplan nesting exceeds the configured depth limit.
|
||||
|
||||
Attributes:
|
||||
depth_limit: Maximum allowed nesting depth.
|
||||
current_depth: The depth that was exceeded.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
depth_limit: int,
|
||||
current_depth: int,
|
||||
) -> None:
|
||||
"""Initialize depth-limit error.
|
||||
|
||||
Args:
|
||||
depth_limit: Maximum allowed nesting depth.
|
||||
current_depth: The actual nesting depth encountered.
|
||||
"""
|
||||
super().__init__(
|
||||
f"Subplan nesting depth {current_depth} exceeds limit {depth_limit}"
|
||||
)
|
||||
self.depth_limit = depth_limit
|
||||
self.current_depth = current_depth
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AuthenticationError",
|
||||
"AuthorizationError",
|
||||
@@ -375,6 +485,7 @@ __all__ = [
|
||||
"InvariantViolationError",
|
||||
"LockConflictError",
|
||||
"LockExpiredError",
|
||||
"MaxParallelExceededError",
|
||||
"MigrationNotApprovedError",
|
||||
"MissingConfigurationError",
|
||||
"ModelNotAvailableError",
|
||||
@@ -386,6 +497,9 @@ __all__ = [
|
||||
"ResourceConflictError",
|
||||
"ResourceNotFoundError",
|
||||
"StreamRoutingError",
|
||||
"SubplanDepthLimitError",
|
||||
"SubplanExecutionError",
|
||||
"SubplanSpawnError",
|
||||
"TokenLimitExceededError",
|
||||
"ToolTypeMismatchError",
|
||||
"ValidationError",
|
||||
|
||||
@@ -562,7 +562,10 @@ SpawnMetadata # noqa: B018, F821
|
||||
SpawnEntry # noqa: B018, F821
|
||||
SpawnResult # noqa: B018, F821
|
||||
SpawnValidationResult # noqa: B018, F821
|
||||
SpawnValidationError # noqa: B018, F821
|
||||
SubplanSpawnError # noqa: B018, F821 (v3.3.0, replaced SpawnValidationError)
|
||||
SubplanExecutionError # noqa: B018, F821 (v3.3.0)
|
||||
MaxParallelExceededError # noqa: B018, F821 (v3.3.0)
|
||||
SubplanDepthLimitError # noqa: B018, F821 (v3.3.0)
|
||||
SubplanService # noqa: B018, F821
|
||||
subplan_service # noqa: B018, F821
|
||||
validation_errors # noqa: B018, F821
|
||||
|
||||
Reference in New Issue
Block a user