Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72765d90a0 | |||
| c3e510b961 | |||
| d24ee08f9b | |||
| 018a316768 | |||
| 0d68abf79f | |||
| 3ccf5dffdd | |||
| 17ded1c83c | |||
| 92cb088895 | |||
| 1cbe5c4bdb | |||
| 83d79357d1 | |||
| 8159b53370 | |||
| aaa5390628 | |||
| 66f9b27a0b | |||
| 49cbac29f5 | |||
| 352c33e0ea | |||
| d21665bfc9 | |||
| c3085d7775 | |||
| cac1d79a20 | |||
| 2181b5031b | |||
| f4e9fa3183 | |||
| 6eb7ceb3fd | |||
| 0fe78e7eff | |||
| 1a1ca84db3 | |||
| 9ada0e4de5 | |||
| 6d1ada9a90 | |||
| 8acc08b118 | |||
| 91d497ca03 | |||
| c104425bed | |||
| 0205edea02 | |||
| 48df4ce508 | |||
| 0cc38d1cd1 | |||
| 307bd49f40 | |||
| 835186ede4 | |||
| ce4579b5ff |
@@ -6,6 +6,8 @@ Changed `wf10_batch.robot` to be less likely to create files, and
|
||||
`plan_generation_graph.robot` to give more test answers.
|
||||
|
||||
## [Unreleased]
|
||||
- **feat(cli): add `agents actor context show` command** (#6369 / PR #6622): Adds `agents actor context show <name>` to display a named actor context's summary, messages, metadata, state, and global context. Supports `--format` (rich/json/yaml/plain/table/color) and `--context-dir` options. Returns exit code 1 for non-existent contexts. Hoists `_default_context_base()` into the show module and re-imports it from `actor_context.py` to remove the duplicated base-path resolution. Adds `command=` kwargs to existing `_render_output` calls in `actor_context.py` so JSON/YAML envelopes report the originating subcommand.
|
||||
- **fix(retry): rename RetryPolicyConfig fields to match spec** (#9396 / PR #9452): Renamed `RetryPolicyConfig` fields to their spec-required names: `max_attempts` → `max_retries`, `base_delay` → `retry_delay_seconds`, `backoff_multiplier` → `backoff_factor`, `max_delay` → `max_backoff`. Updated all references in `service_retry_wiring.py` (field map, wait-strategy builder, `execute`, `async_execute`, `wrap_service_method`). Fixed lint errors (F401 unused imports in `acms_context_analysis_steps.py`, E501 overlong line in `retry_policy.py`). Updated all BDD step definitions and Robot Framework integration scripts to use the renamed fields.
|
||||
- **docs(spec): fix checkpoint config key path and trigger name defaults** (#5009 / PR #5163): Corrects the Configuration Reference table entry `sandbox.checkpoint.auto-create-on` → `core.checkpoints.auto-create-on`, matching the implementation in `config_service.py`. Aligns the default trigger-name values (`before_tool_execute`, `after_tool_execute`) with the implementation in `tool/runner.py`, resolving spec–implementation discrepancies identified in issue #5009.
|
||||
- **docs: module guides for Sandbox & Checkpoint, Correction Attempts, and Invariant Reconciliation** (#4848): Added three comprehensive module guides covering purpose, core classes, lifecycle diagrams, exception hierarchies, CLI usage, and ADR links for `SandboxManager`, `CorrectionAttemptManager`, and `InvariantReconciliationActor`. Includes security callouts for `NoSandbox` bypass (permanent writes, no rollback), `guidance` prompt-injection risk, `archived_artifacts_path` provenance, and `non_overridable` global invariant access control.
|
||||
- **feat(context): PriorityContextStrategy** (#9997 / PR #10772): Implements a priority-based context strategy that ranks context fragments by configurable priority scores — default role-based rules (system > tool > user > assistant), exponential recency decay (7-day half-life), and explicit priority tag boost. Supports custom scoring function injection and custom PriorityRule list injection. Registered in the ACMS pipeline under key `priority_context`. `PriorityRule` uses Pydantic `BaseModel` for architecture conformance. Includes 18 BDD scenarios covering all acceptance criteria.
|
||||
@@ -303,6 +305,10 @@ ensuring data is stored with proper parameter values.
|
||||
|
||||
- **Plan Rollback Command** (#8557): Implemented `agents plan rollback <plan-id> [<checkpoint-id>]` for checkpoint-based plan state restoration in Epic #8493. The command restores a plan's sandbox to the state captured at a given checkpoint, discarding all decisions made after that checkpoint. The checkpoint can be specified as an optional positional second argument or via the `--to-checkpoint` named option. Supports `--yes/-y` flag to skip confirmation prompts and `--format/-f` for output format selection (rich/plain/json/yaml). Included with comprehensive BDD test coverage (>= 97%) and spec-aligned output formatting showing rollback summary, changes reverted, impact analysis, and post-rollback state panels.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`agents actor add` enforces `--update` flag for existing actors** (#1500): Added regression tests to `features/actor_add_update_enforcement.feature` and step definitions in `features/steps/actor_add_update_enforcement_steps.py` verifying that re-adding an existing actor without `--update` fails, while re-adding with `--update` succeeds.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Expanded ruff lint scope to cover `.opencode/`** (#10848): The `lint` nox session
|
||||
@@ -1227,6 +1233,13 @@ Documentation Report (Cycle N)` issues every 10 cycles (~3.3 hours). The manager
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Custom resource type YAML format** (#5622): Corrected three inconsistent YAML format
|
||||
examples in `docs/specification.md` to match the canonical format defined in
|
||||
`docs/schema/resource_type.schema.yaml`. Field names updated: `resource_kind`,
|
||||
`cli_args`, `parent_types`, `capabilities.read/write/sandbox/checkpoint`,
|
||||
`handler: module:ClassName`, and `child_types` as flat string list. Added documentation
|
||||
explaining per-child-type auto-discovery configuration in the `auto_discovery` block.
|
||||
|
||||
- **Plan Concurrency Race Condition** (#7989): Fixed critical race condition in `execute_plan()` and
|
||||
`apply_plan()` where concurrent CLI/worker sessions could simultaneously modify the same plan,
|
||||
corrupting plan state. `LockService` is now wired into the plan lifecycle with plan-level advisory
|
||||
|
||||
+4
-1
@@ -24,7 +24,7 @@
|
||||
# Details
|
||||
* HAL 9000 has contributed spec clarifications for layer boundary DI exception, ULID scope, ACMS pipeline contracts, and TUI component interfaces (PR #10451): documented architectural invariants including the DI container exception, clarified ULID identifier scope distinguishing domain entities from internal implementation details, added per-stage protocol contracts for all 10 ACMS pipeline stages with storage tier definitions, budget enforcement protocol, and context assembly output format, and defined public interfaces with verifiable checks for 8 TUI components.
|
||||
|
||||
|
||||
* HAL 9000 has contributed rename of `ActorSelectionOverlay._render` to `_refresh_display` to avoid shadowing Textual Widget internal method (PR #11042).
|
||||
|
||||
Below are some of the specific details of various contributions.
|
||||
|
||||
@@ -56,6 +56,9 @@ Below are some of the specific details of various contributions.
|
||||
* HAL 9000 has contributed the decision recording hook for the Strategize phase (issue #8522): captures every decision point with question, chosen option, alternatives, confidence, rationale, and full context snapshot for replay and correction.
|
||||
* HAL 9000 has contributed the ContextStrategy protocol and StrategyRegistry plugin registration system (PR #10590 / issue #8616): implemented the pluggable context assembly strategy protocol with proper type-safe method signatures, created the central thread-safe StrategyRegistry supporting registration, lookup, entry-point discovery, and per-strategy configuration (timeout, fragments limits, workers, circuit breaker threshold). Six built-in strategies implemented and documented: simple-keyword, semantic-embedding, breadth-depth-navigator, arce, temporal-archaeology, and plan-decision-context. Full BDD test coverage including thread safety, boundary validation, and error handling tests. (Part of Epic #8505)
|
||||
* HAL9000 has contributed automated implementation of ACMS context policy configuration loader and plan execution integration.
|
||||
* HAL 9000 has contributed automated bug fixes, security improvements, and migration safety enhancements including the migration prompt safe-default fix (#7503).
|
||||
* HAL 9000 has contributed the actor add --update flag enforcement tests and BDD coverage for issue #1500: added regression scenario tests verifying that re-adding an existing actor without `--update` fails, while re-adding with `--update` succeeds.
|
||||
|
||||
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
|
||||
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
|
||||
* HAL 9000 has contributed the pr-review-pool-supervisor tracking prefix documentation fix (#7891): aligned all documentation references from the outdated `AUTO-REV-POOL` prefix to the correct `AUTO-REV-SUP` prefix used in production.
|
||||
|
||||
+30
-29
@@ -24676,17 +24676,16 @@ Custom resource types are defined in YAML configuration files and registered via
|
||||
<span style="color: cyan; font-weight: 600;">resource_type</span>:
|
||||
<span style="color: cyan; font-weight: 600;">name</span>: local/database
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"A SQL database (PostgreSQL, MySQL, SQLite, etc.)"</span>
|
||||
<span style="color: cyan; font-weight: 600;">physical_or_virtual</span>: physical
|
||||
<span style="color: cyan; font-weight: 600;">resource_kind</span>: physical
|
||||
<span style="color: cyan; font-weight: 600;">user_addable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
|
||||
<span style="opacity: 0.7;"> # CLI arguments for `agents resource add local/database`</span>
|
||||
<span style="color: cyan; font-weight: 600;">cli_arguments</span>:
|
||||
<span style="color: cyan; font-weight: 600;">cli_args</span>:
|
||||
- <span style="color: cyan;">name</span>: connection-string
|
||||
<span style="color: cyan; font-weight: 600;">type</span>: string
|
||||
<span style="color: cyan; font-weight: 600;">required</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Database connection string (e.g., postgresql://host/dbname)"</span>
|
||||
<span style="color: cyan; font-weight: 600;">validation</span>:
|
||||
<span style="color: cyan; font-weight: 600;">pattern</span>: <span style="color: #66cc66;">"^(postgresql|mysql|sqlite)://"</span>
|
||||
<span style="color: cyan; font-weight: 600;">validation_pattern</span>: <span style="color: #66cc66;">"^(postgresql|mysql|sqlite)://"</span>
|
||||
- <span style="color: cyan;">name</span>: schema
|
||||
<span style="color: cyan; font-weight: 600;">type</span>: string
|
||||
<span style="color: cyan; font-weight: 600;">required</span>: <span style="color: magenta; font-weight: 600;">false</span>
|
||||
@@ -24699,31 +24698,33 @@ Custom resource types are defined in YAML configuration files and registered via
|
||||
|
||||
<span style="opacity: 0.7;"> # Sandbox and handler</span>
|
||||
<span style="color: cyan; font-weight: 600;">sandbox_strategy</span>: transaction_rollback
|
||||
<span style="color: cyan; font-weight: 600;">handler</span>: DatabaseHandler
|
||||
<span style="color: cyan; font-weight: 600;">checkpointable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">handler</span>: myorg.handlers:DatabaseHandler
|
||||
|
||||
<span style="opacity: 0.7;"> # Allowed parent types (empty means can be top-level)</span>
|
||||
<span style="color: cyan; font-weight: 600;">allowed_parent_types</span>: []
|
||||
<span style="color: cyan; font-weight: 600;">parent_types</span>: []
|
||||
|
||||
<span style="opacity: 0.7;"> # Child types</span>
|
||||
<span style="color: cyan; font-weight: 600;">child_types</span>:
|
||||
- <span style="color: cyan;">type</span>: local/db-schema
|
||||
<span style="color: cyan; font-weight: 600;">auto_discover</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">manual_link</span>: <span style="color: magenta; font-weight: 600;">false</span>
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Discovered database schemas"</span>
|
||||
- <span style="color: cyan;">type</span>: local/db-table
|
||||
<span style="color: cyan; font-weight: 600;">auto_discover</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">manual_link</span>: <span style="color: magenta; font-weight: 600;">false</span>
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Discovered tables within schemas"</span>
|
||||
- local/db-schema
|
||||
- local/db-table
|
||||
|
||||
<span style="opacity: 0.7;"> # Per-child-type discovery configuration lives in `auto_discovery`</span>
|
||||
<span style="color: cyan; font-weight: 600;">auto_discovery</span>:
|
||||
<span style="color: cyan; font-weight: 600;">enabled</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">strategy</span>: schema_scan
|
||||
<span style="color: cyan; font-weight: 600;">manual_link</span>: <span style="color: magenta; font-weight: 600;">false</span>
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Automatically discover schemas and tables for child resources"</span>
|
||||
|
||||
<span style="opacity: 0.7;"> # Capabilities</span>
|
||||
<span style="color: cyan; font-weight: 600;">capabilities</span>:
|
||||
<span style="color: cyan; font-weight: 600;">readable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">writable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">sandboxable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">checkpointable</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">read</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">write</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">sandbox</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">checkpoint</span>: <span style="color: magenta; font-weight: 600;">true</span>
|
||||
</code></pre></div>
|
||||
|
||||
The `auto_discovery` block replaces the former per-child `auto_discover` / `manual_link` YAML objects. Handlers expose their discovery knobs there while `child_types` remains a flat list of allowed resource type names.
|
||||
|
||||
When this type is registered:
|
||||
<div class="highlight"><pre><code>
|
||||
agents resource type add <span style="color: cyan;">--config</span> ./resource-types/database.yaml
|
||||
@@ -25210,11 +25211,11 @@ class ResourceTypeRecord {
|
||||
+ name : String
|
||||
+ description : String
|
||||
+ source : String
|
||||
+ physical_or_virtual : PhysVirt
|
||||
+ resource_kind : PhysVirt
|
||||
+ user_addable : Boolean
|
||||
+ cli_arguments : List<CLIArgument>
|
||||
+ allowed_parent_types : List<String>
|
||||
+ child_types : Map<String, ChildTypeConfig>
|
||||
+ cli_args : List<CLIArgument>
|
||||
+ parent_types : List<String>
|
||||
+ child_types : List<String>
|
||||
+ sandbox_strategy : String
|
||||
+ handler : String
|
||||
+ capabilities : Capabilities
|
||||
@@ -46864,11 +46865,11 @@ New resource types extend CleverAgents to manage any kind of resource:
|
||||
<span style="color: cyan; font-weight: 600;">resource_type</span>:
|
||||
<span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">local/docker-registry</span>
|
||||
<span style="color: cyan; font-weight: 600;">description</span>: <span style="color: #66cc66;">"Docker container registry"</span>
|
||||
<span style="color: cyan; font-weight: 600;">classification</span>: <span style="color: #66cc66;">physical</span>
|
||||
<span style="color: cyan; font-weight: 600;">resource_kind</span>: <span style="color: #66cc66;">physical</span>
|
||||
<span style="color: cyan; font-weight: 600;">user_addable</span>: <span style="color: #66cc66;">true</span>
|
||||
|
||||
<span style="color: cyan; font-weight: 600;">cli_args</span>:
|
||||
- <span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">registry_url</span>
|
||||
- <span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">registry-url</span>
|
||||
<span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">string</span>
|
||||
<span style="color: cyan; font-weight: 600;">required</span>: <span style="color: #66cc66;">true</span>
|
||||
- <span style="color: cyan; font-weight: 600;">name</span>: <span style="color: #66cc66;">repository</span>
|
||||
@@ -46879,16 +46880,16 @@ New resource types extend CleverAgents to manage any kind of resource:
|
||||
<span style="color: cyan; font-weight: 600;">default</span>: <span style="color: #66cc66;">"latest"</span>
|
||||
|
||||
<span style="color: cyan; font-weight: 600;">sandbox_strategy</span>: <span style="color: #66cc66;">none</span> <span style="color: #888;"># Docker images are immutable</span>
|
||||
<span style="color: cyan; font-weight: 600;">handler</span>: <span style="color: #66cc66;">docker_registry_handler</span>
|
||||
<span style="color: cyan; font-weight: 600;">handler</span>: <span style="color: #66cc66;">myorg.handlers:DockerRegistryHandler</span>
|
||||
|
||||
<span style="color: cyan; font-weight: 600;">child_types</span>:
|
||||
- <span style="color: cyan; font-weight: 600;">type</span>: <span style="color: #66cc66;">local/docker-image</span>
|
||||
<span style="color: cyan; font-weight: 600;">relationship</span>: <span style="color: #66cc66;">contains</span>
|
||||
<span style="color: cyan; font-weight: 600;">auto_discover</span>: <span style="color: #66cc66;">true</span>
|
||||
- <span style="color: #66cc66;">local/docker-image</span>
|
||||
|
||||
<span style="opacity: 0.7;"> # Auto-discovery controls per-child discovery behavior</span>
|
||||
<span style="color: cyan; font-weight: 600;">auto_discovery</span>:
|
||||
<span style="color: cyan; font-weight: 600;">enabled</span>: <span style="color: #66cc66;">true</span>
|
||||
<span style="color: cyan; font-weight: 600;">strategy</span>: <span style="color: #66cc66;">list_tags</span>
|
||||
<span style="color: cyan; font-weight: 600;">manual_link</span>: <span style="color: #66cc66;">false</span>
|
||||
</code></pre></div>
|
||||
|
||||
#### Custom Index Backends
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Regression tests for bug #2609: actor add must reject re-adding an existing
|
||||
# actor unless --update is provided.
|
||||
# Regression tests for bug #1500/#2609: actor add must reject re-adding an
|
||||
# existing actor unless --update is provided.
|
||||
Feature: agents actor add enforces --update flag for existing actors
|
||||
As a user of the CleverAgents CLI
|
||||
I want `agents actor add` to fail with a clear error when re-adding an existing actor
|
||||
So that I cannot accidentally overwrite actor configurations without explicit intent
|
||||
|
||||
@tdd_issue @tdd_issue_2609 @tdd_issue_4178
|
||||
@tdd_issue @tdd_issue_1500 @tdd_issue_2609 @tdd_issue_4178
|
||||
Scenario: Re-adding an existing actor without --update fails with error panel
|
||||
Given an actor add CLI runner where the actor already exists
|
||||
When I run actor add without the --update flag
|
||||
@@ -14,7 +14,7 @@ Feature: agents actor add enforces --update flag for existing actors
|
||||
And the actor-add-enforcement output should contain "Use --update to replace the existing actor definition"
|
||||
And the actor-add-enforcement output should contain the registration timestamp
|
||||
|
||||
@tdd_issue @tdd_issue_2609 @tdd_issue_4178
|
||||
@tdd_issue @tdd_issue_1500 @tdd_issue_2609 @tdd_issue_4178
|
||||
Scenario: Re-adding an existing actor without --update shows error status line
|
||||
Given an actor add CLI runner where the actor already exists
|
||||
When I run actor add without the --update flag
|
||||
@@ -22,14 +22,14 @@ Feature: agents actor add enforces --update flag for existing actors
|
||||
And the actor-add-enforcement output should contain "Actor already registered"
|
||||
And the actor-add-enforcement output should contain "use --update to replace"
|
||||
|
||||
@tdd_issue @tdd_issue_2609 @tdd_issue_4178
|
||||
@tdd_issue @tdd_issue_1500 @tdd_issue_2609 @tdd_issue_4178
|
||||
Scenario: Re-adding an existing actor with --update succeeds
|
||||
Given an actor add CLI runner where the actor already exists
|
||||
When I run actor add with the --update flag
|
||||
Then the actor-add-enforcement exit code should be 0
|
||||
And the actor-add-enforcement output should contain "Actor updated"
|
||||
|
||||
@tdd_issue @tdd_issue_2609 @tdd_issue_4178
|
||||
@tdd_issue @tdd_issue_1500 @tdd_issue_2609 @tdd_issue_4178
|
||||
Scenario: Adding a new actor without --update succeeds
|
||||
Given an actor add CLI runner where the actor does not exist
|
||||
When I run actor add without the --update flag
|
||||
|
||||
@@ -135,3 +135,24 @@ Feature: Actor context clear, remove, export, and import commands
|
||||
And I import the context from that JSON file as "roundtrip"
|
||||
Then the context "roundtrip" should exist
|
||||
And the imported context should have the same messages as the original
|
||||
|
||||
|
||||
# ── context show ───────────────────────────────────────────
|
||||
|
||||
Scenario: Show a context in rich format
|
||||
Given an actor context named "docs" exists with messages
|
||||
When I run actor context show "docs" without specifying format
|
||||
Then the actor context show command should succeed
|
||||
And the output should include a context summary for "docs"
|
||||
|
||||
Scenario: Show a context in JSON format
|
||||
Given an actor context named "docs" exists with messages
|
||||
When I run actor context show "docs" with format "json"
|
||||
Then the actor context show command should succeed
|
||||
And the output should contain valid JSON with key "context_summary"
|
||||
And the JSON payload should list 3 messages
|
||||
And the JSON payload metadata context_name should be "docs"
|
||||
|
||||
Scenario: Show non-existent context fails
|
||||
When I run actor context show "ghost" that does not exist
|
||||
Then the actor context show command should fail with exit code 1
|
||||
|
||||
@@ -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 @@
|
||||
"""Step definitions for ACMS context analysis (stub)."""
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Step definitions for actor add --update flag enforcement (issue #2609).
|
||||
"""Step definitions for actor add --update flag enforcement (issue #1500/#2609).
|
||||
|
||||
Tests that `agents actor add` rejects re-adding an existing actor without
|
||||
the --update flag, and succeeds when --update is provided.
|
||||
|
||||
The ``add`` command requires a positional NAME argument followed by
|
||||
``--config <FILE>``:
|
||||
agents actor add <NAME> --config <FILE> [--update]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -105,6 +109,7 @@ def step_when_add_without_update(context: Any) -> None:
|
||||
)
|
||||
registry.upsert_actor.return_value = context.new_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
# The add command requires a positional NAME argument before --config
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
["add", context.actor_name, "--config", str(context.actor_config_path)],
|
||||
@@ -120,6 +125,7 @@ def step_when_add_with_update(context: Any) -> None:
|
||||
# upsert_actor returns the updated actor
|
||||
registry.upsert_actor.return_value = context.updated_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
# The add command requires a positional NAME argument before --config
|
||||
context.result = context.runner.invoke(
|
||||
actor_app,
|
||||
[
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# pyright: reportRedeclaration=false
|
||||
"""Step definitions for actor context remove/export/import commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -331,6 +330,42 @@ def step_import_with_update(context, name):
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When — show
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I run actor context show "{name}" without specifying format')
|
||||
def step_show_context_default(context, name):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
["show", name, "--context-dir", str(context.context_dir)],
|
||||
)
|
||||
|
||||
|
||||
@when('I run actor context show "{name}" with format "{fmt}"')
|
||||
def step_show_context_format(context, name, fmt):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
[
|
||||
"show",
|
||||
name,
|
||||
"--context-dir",
|
||||
str(context.context_dir),
|
||||
"--format",
|
||||
fmt,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@when('I run actor context show "{name}" that does not exist')
|
||||
def step_show_nonexistent_context(context, name):
|
||||
context.result = context.runner.invoke(
|
||||
actor_context_app,
|
||||
["show", name, "--context-dir", str(context.context_dir)],
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# When — round-trip helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -419,6 +454,15 @@ def step_import_success(context):
|
||||
)
|
||||
|
||||
|
||||
@then("the actor context show command should succeed")
|
||||
def step_show_success(context):
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit 0, got {context.result.exit_code}.\n"
|
||||
f"stdout: {context.result.output}\n"
|
||||
f"stderr: {getattr(context.result, 'stderr', '')}"
|
||||
)
|
||||
|
||||
|
||||
@then("the actor context clear command should fail with exit code 1")
|
||||
def step_clear_fail(context):
|
||||
assert context.result.exit_code == 1, (
|
||||
@@ -455,6 +499,15 @@ def step_import_fail(context):
|
||||
)
|
||||
|
||||
|
||||
@then("the actor context show command should fail with exit code 1")
|
||||
def step_show_fail(context):
|
||||
assert context.result.exit_code == 1, (
|
||||
f"Expected exit 1, got {context.result.exit_code}.\n"
|
||||
f"stdout: {context.result.output}\n"
|
||||
f"stderr: {getattr(context.result, 'stderr', '')}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Then — state assertions
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -522,6 +575,35 @@ def step_output_json_key(context, key):
|
||||
assert key in data, f"Key '{key}' not found in JSON output: {data.keys()}"
|
||||
|
||||
|
||||
@then('the output should include a context summary for "{name}"')
|
||||
def step_output_contains_summary(context, name):
|
||||
output = context.result.output
|
||||
assert "Context Summary" in output, (
|
||||
f"Context Summary not found in output:\n{output}"
|
||||
)
|
||||
assert name in output, f"Context name '{name}' not found in output:\n{output}"
|
||||
|
||||
|
||||
@then("the JSON payload should list {count:d} messages")
|
||||
def step_json_message_count(context, count):
|
||||
parsed = json.loads(context.result.output)
|
||||
data = _unwrap_envelope(parsed)
|
||||
messages = data.get("messages")
|
||||
assert isinstance(messages, list), "messages is not a list"
|
||||
assert len(messages) == count, f"Expected {count} messages, found {len(messages)}"
|
||||
|
||||
|
||||
@then('the JSON payload metadata context_name should be "{name}"')
|
||||
def step_json_metadata_context_name(context, name):
|
||||
parsed = json.loads(context.result.output)
|
||||
data = _unwrap_envelope(parsed)
|
||||
metadata = data.get("metadata")
|
||||
assert isinstance(metadata, dict), "metadata is not a dict"
|
||||
assert metadata.get("context_name") == name, (
|
||||
f"Expected metadata.context_name={name!r}, got {metadata.get('context_name')!r}"
|
||||
)
|
||||
|
||||
|
||||
@then('the output JSON key "{top_key}" should contain keys "{keys}"')
|
||||
def step_output_json_nested_keys(context, top_key, keys):
|
||||
parsed = json.loads(context.result.output)
|
||||
|
||||
@@ -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
|
||||
@@ -246,7 +246,7 @@ def step_verify_invalid_merged_skipped(context):
|
||||
def step_verify_original_max_attempts(context):
|
||||
"""Verify the policy was not corrupted by the invalid override."""
|
||||
policy = context.registry.get("plan_service")
|
||||
assert policy.retry.max_attempts == context.original_policy.retry.max_attempts
|
||||
assert policy.retry.max_retries == context.original_policy.retry.max_retries
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -380,7 +380,7 @@ def step_register_custom_policy(context, svc, attempts):
|
||||
policy = ServiceRetryPolicy(
|
||||
service_name=svc,
|
||||
retry_category=RetryCategory.NETWORK,
|
||||
retry=RetryPolicyConfig(max_attempts=attempts),
|
||||
retry=RetryPolicyConfig(max_retries=attempts),
|
||||
description=f"Custom policy for {svc}",
|
||||
)
|
||||
context.registry.register(policy)
|
||||
@@ -391,7 +391,7 @@ def step_verify_registered_policy(context, svc, attempts):
|
||||
"""Verify the registered policy can be retrieved with correct values."""
|
||||
policy = context.registry.get(svc)
|
||||
assert policy.service_name == svc
|
||||
assert policy.retry.max_attempts == attempts
|
||||
assert policy.retry.max_retries == attempts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -43,17 +43,17 @@ def step_create_default_retry_policy(context: Any) -> None:
|
||||
|
||||
@then("the retry policy should have max_attempts {value:d}")
|
||||
def step_check_max_attempts(context: Any, value: int) -> None:
|
||||
assert context.retry_policy.max_attempts == value
|
||||
assert context.retry_policy.max_retries == value
|
||||
|
||||
|
||||
@then("the retry policy should have base_delay {value:g}")
|
||||
def step_check_base_delay(context: Any, value: float) -> None:
|
||||
assert context.retry_policy.base_delay == value
|
||||
assert context.retry_policy.retry_delay_seconds == value
|
||||
|
||||
|
||||
@then("the retry policy should have max_delay {value:g}")
|
||||
def step_check_max_delay(context: Any, value: float) -> None:
|
||||
assert context.retry_policy.max_delay == value
|
||||
assert context.retry_policy.max_backoff == value
|
||||
|
||||
|
||||
@then("the retry policy should have jitter {value}")
|
||||
@@ -70,7 +70,9 @@ def step_check_backoff_strategy_exp(context: Any) -> None:
|
||||
def step_create_invalid_retry_policy(context: Any, base: float, mx: float) -> None:
|
||||
context.validation_error = None
|
||||
try:
|
||||
context.retry_policy = RetryPolicyConfig(base_delay=base, max_delay=mx)
|
||||
context.retry_policy = RetryPolicyConfig(
|
||||
retry_delay_seconds=base, max_backoff=mx
|
||||
)
|
||||
except ValidationError as exc:
|
||||
context.validation_error = exc
|
||||
|
||||
@@ -173,7 +175,7 @@ def step_check_desc_contains(context: Any, text: str) -> None:
|
||||
@when("I apply overrides setting plan_service max_attempts to {value:d}")
|
||||
def step_apply_overrides(context: Any, value: int) -> None:
|
||||
context.registry.apply_overrides(
|
||||
{"plan_service": {"retry": {"max_attempts": value}}}
|
||||
{"plan_service": {"retry": {"max_retries": value}}}
|
||||
)
|
||||
|
||||
|
||||
@@ -181,7 +183,7 @@ def step_apply_overrides(context: Any, value: int) -> None:
|
||||
def step_check_plan_service_max(context: Any, value: int) -> None:
|
||||
registry = getattr(context, "registry", None) or context.wiring.registry
|
||||
policy = registry.get("plan_service")
|
||||
assert policy.retry.max_attempts == value
|
||||
assert policy.retry.max_retries == value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -264,7 +266,7 @@ def step_check_all_policies_dict(context: Any) -> None:
|
||||
def step_create_invalid_max_attempts(context: Any, value: int) -> None:
|
||||
context.neg_validation_error = None
|
||||
try:
|
||||
RetryPolicyConfig(max_attempts=value)
|
||||
RetryPolicyConfig(max_retries=value)
|
||||
except ValidationError as exc:
|
||||
context.neg_validation_error = exc
|
||||
|
||||
@@ -319,7 +321,7 @@ def step_check_neg_policy_validation(context: Any) -> None:
|
||||
@when('I apply overrides with service_name mutation for "{name}"')
|
||||
def step_apply_overrides_with_name_mutation(context: Any, name: str) -> None:
|
||||
context.registry.apply_overrides(
|
||||
{name: {"service_name": "hijacked_name", "retry": {"max_attempts": 4}}}
|
||||
{name: {"service_name": "hijacked_name", "retry": {"max_retries": 4}}}
|
||||
)
|
||||
|
||||
|
||||
@@ -332,15 +334,15 @@ def step_check_no_name_mutation(context: Any, name: str) -> None:
|
||||
@when('I apply overrides with invalid data for "{name}"')
|
||||
def step_apply_invalid_overrides(context: Any, name: str) -> None:
|
||||
original = context.registry.get(name)
|
||||
context.original_max_attempts = original.retry.max_attempts
|
||||
# max_attempts=0 violates ge=1 constraint -> ValidationError
|
||||
context.registry.apply_overrides({name: {"retry": {"max_attempts": 0}}})
|
||||
context.original_max_attempts = original.retry.max_retries
|
||||
# max_retries=0 violates ge=1 constraint -> ValidationError
|
||||
context.registry.apply_overrides({name: {"retry": {"max_retries": 0}}})
|
||||
|
||||
|
||||
@then("the plan_service policy should retain its original max_attempts")
|
||||
def step_check_original_retained(context: Any) -> None:
|
||||
policy = context.registry.get("plan_service")
|
||||
assert policy.retry.max_attempts == context.original_max_attempts
|
||||
assert policy.retry.max_retries == context.original_max_attempts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -376,7 +378,7 @@ def step_apply_non_dict_overrides(context: Any, name: str) -> None:
|
||||
@then("the plan_service policy should be unchanged after non-dict override")
|
||||
def step_check_policy_unchanged_after_non_dict(context: Any) -> None:
|
||||
policy = context.registry.get("plan_service")
|
||||
assert policy.retry.max_attempts == context.original_policy.retry.max_attempts
|
||||
assert policy.retry.max_retries == context.original_policy.retry.max_retries
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -394,15 +396,15 @@ def step_mutate_policy(context: Any) -> None:
|
||||
policy = context.fresh_registry.get("plan_service")
|
||||
context.original_session_max = context.fresh_registry.get(
|
||||
"session_service"
|
||||
).retry.max_attempts
|
||||
).retry.max_retries
|
||||
# Mutate plan_service's retry config
|
||||
policy.retry.max_attempts = 99
|
||||
policy.retry.max_retries = 99
|
||||
|
||||
|
||||
@then("other service policies should not be affected by the mutation")
|
||||
def step_check_no_cross_mutation(context: Any) -> None:
|
||||
session_policy = context.fresh_registry.get("session_service")
|
||||
assert session_policy.retry.max_attempts == context.original_session_max
|
||||
assert session_policy.retry.max_retries == context.original_session_max
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -420,17 +422,17 @@ def step_get_two_unknown_policies(context: Any) -> None:
|
||||
def step_mutate_first_unknown(context: Any) -> None:
|
||||
from cleveragents.domain.models.core.retry_policy import DEFAULT_DATABASE_RETRY
|
||||
|
||||
context.default_max_before = DEFAULT_DATABASE_RETRY.max_attempts
|
||||
context.unknown_b_max_before = context.unknown_b.retry.max_attempts
|
||||
context.default_max_before = DEFAULT_DATABASE_RETRY.max_retries
|
||||
context.unknown_b_max_before = context.unknown_b.retry.max_retries
|
||||
# Mutate only the first unknown service's retry config
|
||||
context.unknown_a.retry.max_attempts = 77
|
||||
context.unknown_a.retry.max_retries = 77
|
||||
|
||||
|
||||
@then("the second unknown service retry config should be unaffected")
|
||||
def step_check_second_unknown_unaffected(context: Any) -> None:
|
||||
assert context.unknown_b.retry.max_attempts == context.unknown_b_max_before, (
|
||||
assert context.unknown_b.retry.max_retries == context.unknown_b_max_before, (
|
||||
f"Second unknown service was corrupted: "
|
||||
f"{context.unknown_b.retry.max_attempts} != {context.unknown_b_max_before}"
|
||||
f"{context.unknown_b.retry.max_retries} != {context.unknown_b_max_before}"
|
||||
)
|
||||
|
||||
|
||||
@@ -438,7 +440,7 @@ def step_check_second_unknown_unaffected(context: Any) -> None:
|
||||
def step_check_default_database_retry_unaffected(context: Any) -> None:
|
||||
from cleveragents.domain.models.core.retry_policy import DEFAULT_DATABASE_RETRY
|
||||
|
||||
assert DEFAULT_DATABASE_RETRY.max_attempts == context.default_max_before, (
|
||||
assert DEFAULT_DATABASE_RETRY.max_retries == context.default_max_before, (
|
||||
f"DEFAULT_DATABASE_RETRY was corrupted: "
|
||||
f"{DEFAULT_DATABASE_RETRY.max_attempts} != {context.default_max_before}"
|
||||
f"{DEFAULT_DATABASE_RETRY.max_retries} != {context.default_max_before}"
|
||||
)
|
||||
|
||||
@@ -130,7 +130,7 @@ def step_create_wiring_base_delay(context: Any) -> None:
|
||||
@then("the plan_service policy should have base_delay {value:g}")
|
||||
def step_check_base_delay_override(context: Any, value: float) -> None:
|
||||
policy = context.wiring.get_policy("plan_service")
|
||||
assert policy.retry.base_delay == value
|
||||
assert policy.retry.retry_delay_seconds == value
|
||||
|
||||
|
||||
@given("I have Settings with non-default circuit_breaker_recovery_timeout {value:g}")
|
||||
@@ -228,7 +228,7 @@ def step_create_wiring_max_delay(context: Any) -> None:
|
||||
@then("the plan_service policy should have max_delay {value:g}")
|
||||
def step_check_max_delay_override(context: Any, value: float) -> None:
|
||||
policy = context.wiring.get_policy("plan_service")
|
||||
assert policy.retry.max_delay == value
|
||||
assert policy.retry.max_backoff == value
|
||||
|
||||
|
||||
@given("I have Settings with non-default retry_jitter {value}")
|
||||
|
||||
@@ -210,7 +210,7 @@ def step_check_async_exhaustion_error(context: Any) -> None:
|
||||
@then("the async always-failing function should have been called max_attempts times")
|
||||
def step_check_async_always_fail_count(context: Any) -> None:
|
||||
policy = context.wiring.get_policy("plan_service")
|
||||
assert context.async_call_count == policy.retry.max_attempts
|
||||
assert context.async_call_count == policy.retry.max_retries
|
||||
|
||||
|
||||
@then("the captured log should contain a retry warning message")
|
||||
|
||||
@@ -86,7 +86,7 @@ def step_settings_non_default_max_attempts(context, n):
|
||||
|
||||
@given('settings with retry_service_overrides introducing a new service "{svc_name}"')
|
||||
def step_settings_with_new_service_override(context, svc_name):
|
||||
overrides = {svc_name: {"retry": {"base_delay": 0.5, "max_delay": 5.0}}}
|
||||
overrides = {svc_name: {"retry": {"retry_delay_seconds": 0.5, "max_backoff": 5.0}}}
|
||||
context.custom_settings = _make_settings_via_env(
|
||||
retry_max_attempts=context.custom_max_attempts,
|
||||
retry_service_overrides=json.dumps(overrides),
|
||||
@@ -101,8 +101,8 @@ def step_create_wiring_from_settings(context):
|
||||
@then('the policy for "{svc_name}" should have max_attempts of {n:d}')
|
||||
def step_verify_policy_max_attempts(context, svc_name, n):
|
||||
policy = context.wiring.get_policy(svc_name)
|
||||
assert policy.retry.max_attempts == n, (
|
||||
f"Expected max_attempts={n}, got {policy.retry.max_attempts}"
|
||||
assert policy.retry.max_retries == n, (
|
||||
f"Expected max_retries={n}, got {policy.retry.max_retries}"
|
||||
)
|
||||
|
||||
|
||||
@@ -165,9 +165,9 @@ def step_policy_with_string_backoff(context, strategy):
|
||||
policy = ServiceRetryPolicy(
|
||||
service_name="string_backoff_svc",
|
||||
retry=RetryPolicyConfig(
|
||||
max_attempts=2,
|
||||
base_delay=0.1,
|
||||
max_delay=1.0,
|
||||
max_retries=2,
|
||||
retry_delay_seconds=0.1,
|
||||
max_backoff=1.0,
|
||||
backoff_strategy=RetryStrategy.EXPONENTIAL,
|
||||
),
|
||||
)
|
||||
@@ -198,7 +198,11 @@ def step_wiring_with_disabled_cb(context):
|
||||
# Register a service with CB disabled
|
||||
disabled_cb_policy = ServiceRetryPolicy(
|
||||
service_name="no_cb_service",
|
||||
retry=RetryPolicyConfig(max_attempts=2, base_delay=0.01, max_delay=0.1),
|
||||
retry=RetryPolicyConfig(
|
||||
max_retries=2,
|
||||
retry_delay_seconds=0.01,
|
||||
max_backoff=0.1,
|
||||
),
|
||||
circuit_breaker=CircuitBreakerConfig(enabled=False),
|
||||
)
|
||||
context.wiring._registry.register(disabled_cb_policy)
|
||||
@@ -231,7 +235,11 @@ def step_concurrent_cb_request(context):
|
||||
# Ensure the service has a policy with CB enabled
|
||||
policy = ServiceRetryPolicy(
|
||||
service_name=svc_name,
|
||||
retry=RetryPolicyConfig(max_attempts=2, base_delay=0.01, max_delay=0.1),
|
||||
retry=RetryPolicyConfig(
|
||||
max_retries=2,
|
||||
retry_delay_seconds=0.01,
|
||||
max_backoff=0.1,
|
||||
),
|
||||
circuit_breaker=CircuitBreakerConfig(enabled=True),
|
||||
)
|
||||
context.wiring._registry.register(policy)
|
||||
@@ -438,9 +446,9 @@ def step_wiring_with_string_backoff(context):
|
||||
policy = ServiceRetryPolicy(
|
||||
service_name="str_backoff_wrap_svc",
|
||||
retry=RetryPolicyConfig(
|
||||
max_attempts=2,
|
||||
base_delay=0.01,
|
||||
max_delay=0.1,
|
||||
max_retries=2,
|
||||
retry_delay_seconds=0.01,
|
||||
max_backoff=0.1,
|
||||
backoff_strategy=RetryStrategy.EXPONENTIAL,
|
||||
),
|
||||
circuit_breaker=CircuitBreakerConfig(enabled=False),
|
||||
|
||||
@@ -172,7 +172,7 @@ def step_settings_with_overrides(context: Any, value: int) -> None:
|
||||
import json
|
||||
import os
|
||||
|
||||
overrides = json.dumps({"plan_service": {"retry": {"max_attempts": value}}})
|
||||
overrides = json.dumps({"plan_service": {"retry": {"max_retries": value}}})
|
||||
# Settings uses validation_alias so the field must be set via env var
|
||||
os.environ["CLEVERAGENTS_RETRY_SERVICE_OVERRIDES"] = overrides
|
||||
try:
|
||||
@@ -298,7 +298,7 @@ def step_check_exhaustion_error(context: Any) -> None:
|
||||
@then("the always-failing function should have been called max_attempts times")
|
||||
def step_check_always_fail_call_count(context: Any) -> None:
|
||||
policy = context.wiring.get_policy("plan_service")
|
||||
assert context.always_fail_count == policy.retry.max_attempts
|
||||
assert context.always_fail_count == policy.retry.max_retries
|
||||
|
||||
|
||||
@when("I decorate and call a function via wrap_service_method")
|
||||
|
||||
@@ -488,3 +488,99 @@ def step_persona_bar_reflects_actor(context: object) -> None:
|
||||
assert "anthropic/claude-sonnet-4-20250514" in bar._text, (
|
||||
f"Expected actor in persona bar, got: {bar._text}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TDD regression: issue #11039 - no _render() shadowing Textual Widget._render
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@then("the overlay should not override Textual Widget._render")
|
||||
def step_overlay_no_render_override(context: object) -> None:
|
||||
"""Verify that ActorSelectionOverlay does NOT define its own ``_render``.
|
||||
|
||||
The Shadowing Bug (issue #11039): ActorSelectionOverlay once defined
|
||||
``def _render(self) -> None`` which returned ``None``, shadowing
|
||||
``textual.widgets.Static._render()`` (which returns a
|
||||
:class:`textual.strip.Strip`). This caused the Textual layout engine to
|
||||
crash with ``AttributeError: 'NoneType' object has no attribute
|
||||
'get_height'``.
|
||||
|
||||
The fix renamed the method to ``_refresh_display`` so that the inherited
|
||||
base-class ``_render`` is untouched and returns a proper renderable.
|
||||
"""
|
||||
from cleveragents.tui.widgets.actor_selection_overlay import (
|
||||
ActorSelectionOverlay,
|
||||
)
|
||||
|
||||
cls = ActorSelectionOverlay
|
||||
# The overlay should NOT define its own _render in its __dict__.
|
||||
# If it did, _render found on the class would resolve to this method
|
||||
# rather than the Textual base-class implementation.
|
||||
has_own_render = "_render" in cls.__dict__
|
||||
assert not has_own_render, (
|
||||
"ActorSelectionOverlay defines its own _render() which shadows "
|
||||
"Textual Widget._render. This is the bug from issue #11039."
|
||||
)
|
||||
|
||||
|
||||
@then("the overlay _refresh_display method should be callable")
|
||||
def step_overlay_refresh_display_callable(context: object) -> None:
|
||||
"""Verify ``_refresh_display`` exists and is callable after show()."""
|
||||
assert hasattr(context._overlay, "_refresh_display"), (
|
||||
"Overlay must have _refresh_display method"
|
||||
)
|
||||
refresh = context._overlay._refresh_display
|
||||
assert callable(refresh), "_refresh_display must be callable"
|
||||
|
||||
|
||||
@then("the inherited Widget._render should return a proper renderable")
|
||||
def step_inherited_render_returns_renderable(context: object) -> None:
|
||||
"""Verify the inherited ``Widget._render()`` returns a non-None renderable.
|
||||
|
||||
Bug #11039: ``ActorSelectionOverlay`` defined its own ``_render() -> None``
|
||||
which returned ``None``, causing Textual's layout engine to crash. The
|
||||
fix renamed it to ``_refresh_display()``. This assertion confirms that
|
||||
when Textual IS available, calling the real inherited ``_render()``
|
||||
produces a proper renderable (not ``None``). When Textual is not
|
||||
available (``_FallbackStatic`` has no ``_render``), the check passes
|
||||
trivially since the fallback case cannot trigger the shadowing crash.
|
||||
"""
|
||||
|
||||
from cleveragents.tui.widgets.actor_selection_overlay import (
|
||||
ActorSelectionOverlay,
|
||||
)
|
||||
|
||||
cls = ActorSelectionOverlay
|
||||
overlay = context._overlay
|
||||
|
||||
render_from_mro = None
|
||||
for base in cls.__mro__[1:]:
|
||||
if "_render" in base.__dict__:
|
||||
render_from_mro = base.__dict__["_render"]
|
||||
break
|
||||
|
||||
if render_from_mro is None:
|
||||
return
|
||||
|
||||
result = render_from_mro(overlay)
|
||||
assert result is not None, (
|
||||
"Bug #11039 regression: inherited Widget._render() returned None. "
|
||||
"ActorSelectionOverlay may still be shadowing the base-class "
|
||||
"_render method, which would cause a NoneType crash during "
|
||||
"Textual layout in textual >=1.0."
|
||||
)
|
||||
|
||||
|
||||
@then("_refresh_display should produce correct overlay content")
|
||||
def step_refresh_display_produces_content(context: object) -> None:
|
||||
"""Invoke ``_refresh_display()`` and verify it renders correct content."""
|
||||
from typing import cast
|
||||
|
||||
overlay = context._overlay
|
||||
overlay._refresh_display()
|
||||
content: str = cast(str, overlay._text)
|
||||
assert content, "_refresh_display must produce non-empty rendered overlay content"
|
||||
assert "Welcome to CleverAgents" in content, (
|
||||
"_refresh_display content must include the welcome header"
|
||||
)
|
||||
|
||||
@@ -194,3 +194,23 @@ Feature: TUI first-run experience with actor selection overlay
|
||||
And I call _complete_first_run with actor "anthropic/claude-sonnet-4-20250514"
|
||||
Then the registry should contain a persona named "default"
|
||||
And the persona bar should reflect the new actor
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# TDD regression: issue #11039 - no _render() shadowing Textual Widget._render
|
||||
# -----------------------------------------------------------------------
|
||||
# The overlay formerly defined its own _render() returning None, which
|
||||
# shadowed textual.widgets.Static._render and caused layout-engine crashes.
|
||||
# This scenario ensures the rename to _refresh_display eliminated that bug.
|
||||
|
||||
@tdd_issue @tdd_issue_11039
|
||||
Scenario: ActorSelectionOverlay does not define its own _render method
|
||||
Given a new ActorSelectionOverlay
|
||||
Then the overlay should not override Textual Widget._render
|
||||
And the inherited Widget._render should return a proper renderable
|
||||
|
||||
@tdd_issue @tdd_issue_11039
|
||||
Scenario: ActorSelectionOverlay has refresh_display callable instead of render
|
||||
Given a new ActorSelectionOverlay
|
||||
When I call show on the overlay
|
||||
Then the overlay _refresh_display method should be callable
|
||||
And _refresh_display should produce correct overlay content
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests for actor add --update flag enforcement (issue #1500)
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER} ${CURDIR}/helper_actor_add_update_enforcement.py
|
||||
|
||||
*** Test Cases ***
|
||||
Actor Add Existing Actor Without Update Flag Fails
|
||||
[Documentation] Verify that actor add rejects re-adding an existing actor without --update
|
||||
[Tags] tdd_issue tdd_issue_1500
|
||||
${result}= Run Process ${PYTHON} ${HELPER} reject-existing-without-update cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-reject-existing-ok
|
||||
|
||||
Actor Add Existing Actor With Update Flag Succeeds
|
||||
[Documentation] Verify that actor add with --update on an existing actor succeeds
|
||||
[Tags] tdd_issue tdd_issue_1500
|
||||
${result}= Run Process ${PYTHON} ${HELPER} accept-existing-with-update cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-accept-update-ok
|
||||
|
||||
Actor Add New Actor Without Update Flag Succeeds
|
||||
[Documentation] Verify that actor add on a new actor succeeds without --update
|
||||
[Tags] tdd_issue tdd_issue_1500
|
||||
${result}= Run Process ${PYTHON} ${HELPER} accept-new-without-update cwd=${WORKSPACE}
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} actor-add-accept-new-ok
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Helper script for actor add --update flag enforcement Robot tests (issue #1500).
|
||||
|
||||
Tests that ``agents actor add`` rejects re-adding an existing actor without
|
||||
the --update flag, and succeeds when --update is provided or the actor is new.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.actor import app as actor_app
|
||||
from cleveragents.core.exceptions import NotFoundError
|
||||
from cleveragents.domain.models.core.actor import Actor
|
||||
|
||||
|
||||
def _make_actor(
|
||||
*,
|
||||
name: str = "local/existing-actor",
|
||||
provider: str = "openai",
|
||||
model: str = "gpt-4",
|
||||
config: dict[str, Any] | None = None,
|
||||
updated_at: datetime | None = None,
|
||||
) -> Actor:
|
||||
blob = config or {}
|
||||
return Actor(
|
||||
id=1,
|
||||
name=name,
|
||||
provider=provider,
|
||||
model=model,
|
||||
config_blob=blob,
|
||||
config_hash=Actor.compute_hash(blob),
|
||||
unsafe=False,
|
||||
is_built_in=False,
|
||||
is_default=False,
|
||||
updated_at=updated_at or datetime(2026, 2, 7, 14, 22, 0),
|
||||
)
|
||||
|
||||
|
||||
def _write_config(name: str) -> Path:
|
||||
config_data: dict[str, Any] = {
|
||||
"name": name,
|
||||
"provider": "openai",
|
||||
"model": "gpt-4",
|
||||
}
|
||||
with tempfile.NamedTemporaryFile(
|
||||
delete=False, suffix=".json", mode="w", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(config_data, handle)
|
||||
handle.flush()
|
||||
return Path(handle.name)
|
||||
|
||||
|
||||
def test_reject_existing_without_update() -> None:
|
||||
"""Re-adding an existing actor without --update must exit 1 with an error panel."""
|
||||
actor_name = "local/existing-actor"
|
||||
config_path = _write_config(actor_name)
|
||||
existing = _make_actor(name=actor_name, updated_at=datetime(2026, 2, 7, 14, 22, 0))
|
||||
|
||||
try:
|
||||
runner = CliRunner()
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.get_actor.return_value = existing
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
result = runner.invoke(
|
||||
actor_app,
|
||||
["add", actor_name, "--config", str(config_path)],
|
||||
)
|
||||
finally:
|
||||
config_path.unlink(missing_ok=True)
|
||||
|
||||
assert result.exit_code == 1, (
|
||||
f"Expected exit code 1 but got {result.exit_code}.\nOutput:\n{result.output}"
|
||||
)
|
||||
assert "Actor already exists" in result.output, (
|
||||
f"Expected 'Actor already exists' in output:\n{result.output}"
|
||||
)
|
||||
assert "Use --update to replace" in result.output, (
|
||||
f"Expected 'Use --update to replace' in output:\n{result.output}"
|
||||
)
|
||||
print("actor-add-reject-existing-ok")
|
||||
|
||||
|
||||
def test_accept_existing_with_update() -> None:
|
||||
"""Re-adding an existing actor with --update must exit 0 and show
|
||||
'Actor updated'."""
|
||||
actor_name = "local/existing-actor"
|
||||
config_path = _write_config(actor_name)
|
||||
existing = _make_actor(name=actor_name, updated_at=datetime(2026, 2, 7, 14, 22, 0))
|
||||
updated = _make_actor(name=actor_name, updated_at=datetime(2026, 2, 7, 14, 22, 0))
|
||||
|
||||
try:
|
||||
runner = CliRunner()
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.get_actor.return_value = existing
|
||||
registry.upsert_actor.return_value = updated
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
result = runner.invoke(
|
||||
actor_app,
|
||||
["add", actor_name, "--config", str(config_path), "--update"],
|
||||
)
|
||||
finally:
|
||||
config_path.unlink(missing_ok=True)
|
||||
|
||||
assert result.exit_code == 0, (
|
||||
f"Expected exit code 0 but got {result.exit_code}.\nOutput:\n{result.output}"
|
||||
)
|
||||
assert "Actor updated" in result.output, (
|
||||
f"Expected 'Actor updated' in output:\n{result.output}"
|
||||
)
|
||||
print("actor-add-accept-update-ok")
|
||||
|
||||
|
||||
def test_accept_new_without_update() -> None:
|
||||
"""Adding a brand-new actor without --update must exit 0 and show 'Actor added'."""
|
||||
actor_name = "local/new-actor"
|
||||
config_path = _write_config(actor_name)
|
||||
new_actor = _make_actor(name=actor_name)
|
||||
|
||||
try:
|
||||
runner = CliRunner()
|
||||
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
|
||||
registry = MagicMock()
|
||||
registry.get_actor.side_effect = NotFoundError(
|
||||
resource_type="actor", resource_id=actor_name
|
||||
)
|
||||
registry.upsert_actor.return_value = new_actor
|
||||
mock_svc.return_value = (MagicMock(), registry)
|
||||
result = runner.invoke(
|
||||
actor_app,
|
||||
["add", actor_name, "--config", str(config_path)],
|
||||
)
|
||||
finally:
|
||||
config_path.unlink(missing_ok=True)
|
||||
|
||||
assert result.exit_code == 0, (
|
||||
f"Expected exit code 0 but got {result.exit_code}.\nOutput:\n{result.output}"
|
||||
)
|
||||
assert "Actor added" in result.output, (
|
||||
f"Expected 'Actor added' in output:\n{result.output}"
|
||||
)
|
||||
print("actor-add-accept-new-ok")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
command = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
dispatch = {
|
||||
"reject-existing-without-update": test_reject_existing_without_update,
|
||||
"accept-existing-with-update": test_accept_existing_with_update,
|
||||
"accept-new-without-update": test_accept_new_without_update,
|
||||
}
|
||||
fn = dispatch.get(command)
|
||||
if fn is None:
|
||||
print(f"Unknown command: {command!r}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
fn()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -170,9 +170,9 @@ Get retry_policy_defaults Script
|
||||
... sys.path.insert(0, '${WORKSPACE_ROOT}/src')
|
||||
... from cleveragents.domain.models.core.retry_policy import RetryPolicyConfig
|
||||
... p = RetryPolicyConfig()
|
||||
... print(f"max_attempts={p.max_attempts}", flush=True)
|
||||
... print(f"base_delay={p.base_delay}", flush=True)
|
||||
... print(f"max_delay={p.max_delay}", flush=True)
|
||||
... print(f"max_attempts={p.max_retries}", flush=True)
|
||||
... print(f"base_delay={p.retry_delay_seconds}", flush=True)
|
||||
... print(f"max_delay={p.max_backoff}", flush=True)
|
||||
... print(f"jitter={p.jitter}", flush=True)
|
||||
... print(f"backoff_strategy={p.backoff_strategy.value}", flush=True)
|
||||
... sys.exit(0)
|
||||
@@ -209,9 +209,9 @@ Get registry_override Script
|
||||
... sys.path.insert(0, '${WORKSPACE_ROOT}/src')
|
||||
... from cleveragents.domain.models.core.retry_policy import ServiceRetryPolicyRegistry
|
||||
... reg = ServiceRetryPolicyRegistry()
|
||||
... reg.apply_overrides({"plan_service": {"retry": {"max_attempts": 7}}})
|
||||
... reg.apply_overrides({"plan_service": {"retry": {"max_retries": 7}}})
|
||||
... p = reg.get("plan_service")
|
||||
... print(f"max_attempts={p.retry.max_attempts}", flush=True)
|
||||
... print(f"max_attempts={p.retry.max_retries}", flush=True)
|
||||
... sys.exit(0)
|
||||
RETURN ${script}
|
||||
|
||||
@@ -304,7 +304,7 @@ Get validation Script
|
||||
... from pydantic import ValidationError
|
||||
... from cleveragents.domain.models.core.retry_policy import RetryPolicyConfig
|
||||
... try:
|
||||
... ${SPACE}${SPACE}${SPACE}${SPACE}RetryPolicyConfig(base_delay=10.0, max_delay=5.0)
|
||||
... ${SPACE}${SPACE}${SPACE}${SPACE}RetryPolicyConfig(retry_delay_seconds=10.0, max_backoff=5.0)
|
||||
... ${SPACE}${SPACE}${SPACE}${SPACE}print("No error", flush=True)
|
||||
... except ValidationError:
|
||||
... ${SPACE}${SPACE}${SPACE}${SPACE}print("ValidationError raised", flush=True)
|
||||
|
||||
@@ -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)
|
||||
@@ -82,9 +82,9 @@ MAX_RETRY_TOTAL_TIMEOUT = 300.0 # 5 minutes
|
||||
# S21: Mapping from Settings field names to (policy_key, group) pairs
|
||||
# used by _apply_settings_defaults to replace repetitive if-comparisons.
|
||||
_RETRY_FIELD_MAP: dict[str, tuple[str, str]] = {
|
||||
"retry_max_attempts": ("max_attempts", "retry"),
|
||||
"retry_base_delay": ("base_delay", "retry"),
|
||||
"retry_max_delay": ("max_delay", "retry"),
|
||||
"retry_max_attempts": ("max_retries", "retry"),
|
||||
"retry_base_delay": ("retry_delay_seconds", "retry"),
|
||||
"retry_max_delay": ("max_backoff", "retry"),
|
||||
"retry_jitter": ("jitter", "retry"),
|
||||
"retry_backoff_strategy": ("backoff_strategy", "retry"),
|
||||
"circuit_breaker_failure_threshold": ("failure_threshold", "circuit_breaker"),
|
||||
@@ -263,8 +263,8 @@ class ServiceRetryWiring:
|
||||
)
|
||||
return _build_wait_strategy(
|
||||
backoff,
|
||||
policy.retry.base_delay,
|
||||
policy.retry.max_delay,
|
||||
policy.retry.retry_delay_seconds,
|
||||
policy.retry.max_backoff,
|
||||
policy.retry.jitter,
|
||||
)
|
||||
|
||||
@@ -416,7 +416,7 @@ class ServiceRetryWiring:
|
||||
try:
|
||||
for attempt_obj in Retrying(
|
||||
stop=(
|
||||
stop_after_attempt(policy.retry.max_attempts)
|
||||
stop_after_attempt(policy.retry.max_retries)
|
||||
| stop_after_delay(MAX_RETRY_TOTAL_TIMEOUT)
|
||||
),
|
||||
wait=wait,
|
||||
@@ -530,7 +530,7 @@ class ServiceRetryWiring:
|
||||
try:
|
||||
async for attempt_obj in AsyncRetrying(
|
||||
stop=(
|
||||
stop_after_attempt(policy.retry.max_attempts)
|
||||
stop_after_attempt(policy.retry.max_retries)
|
||||
| stop_after_delay(MAX_RETRY_TOTAL_TIMEOUT)
|
||||
),
|
||||
wait=wait,
|
||||
@@ -616,9 +616,9 @@ class ServiceRetryWiring:
|
||||
decorator = retry_service_operation(
|
||||
service_name=service_name,
|
||||
operation_name=operation_name,
|
||||
max_attempts=policy.retry.max_attempts,
|
||||
base_delay=policy.retry.base_delay,
|
||||
max_delay=policy.retry.max_delay,
|
||||
max_attempts=policy.retry.max_retries,
|
||||
base_delay=policy.retry.retry_delay_seconds,
|
||||
max_delay=policy.retry.max_backoff,
|
||||
jitter=policy.retry.jitter,
|
||||
backoff_strategy=policy.retry.backoff_strategy.value
|
||||
if isinstance(policy.retry.backoff_strategy, RetryStrategy)
|
||||
|
||||
@@ -24,6 +24,8 @@ from rich.panel import Panel
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.reactive.context_manager import ContextManager
|
||||
|
||||
from .actor_context_show import _default_context_base, register_show_command
|
||||
|
||||
app = typer.Typer(
|
||||
help="Manage manual contexts for actor runs.",
|
||||
)
|
||||
@@ -36,13 +38,6 @@ _FORMAT_HELP = "Output format: json, yaml, plain, table, rich, or color (default
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _default_context_base(context_dir: Path | None) -> Path:
|
||||
"""Return the base directory where named contexts are stored."""
|
||||
if context_dir is not None:
|
||||
return context_dir
|
||||
return Path.home() / ".cleveragents" / "context"
|
||||
|
||||
|
||||
def _list_context_names(base: Path) -> list[str]:
|
||||
"""Return sorted list of context names present under *base*."""
|
||||
if not base.exists():
|
||||
@@ -83,13 +78,13 @@ def _render_output(
|
||||
fmt: str,
|
||||
rich_panels: list[tuple[str, str]] | None = None,
|
||||
ok_message: str = "",
|
||||
*,
|
||||
command: str | None = None,
|
||||
status: str = "ok",
|
||||
exit_code: int = 0,
|
||||
) -> None:
|
||||
"""Emit command output in the requested format.
|
||||
"""Emit command output in the requested format."""
|
||||
|
||||
For ``rich`` format the *rich_panels* list of ``(title, body)`` pairs
|
||||
are rendered via :class:`rich.panel.Panel`. For all other formats the
|
||||
flat *data* dict is passed through :func:`format_output`.
|
||||
"""
|
||||
if fmt == OutputFormat.RICH.value and rich_panels:
|
||||
for title, body in rich_panels:
|
||||
console.print(Panel(body, title=title, expand=False))
|
||||
@@ -97,8 +92,18 @@ def _render_output(
|
||||
console.print(f"[green]✓ OK[/green] {ok_message}")
|
||||
return
|
||||
|
||||
# Machine-readable / non-rich
|
||||
console.print(format_output(data, fmt))
|
||||
console.print(
|
||||
format_output(
|
||||
data,
|
||||
fmt,
|
||||
command=command or "",
|
||||
status=status,
|
||||
exit_code=exit_code,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
register_show_command(app, _render_output, _FORMAT_HELP)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -193,7 +198,13 @@ def context_remove(
|
||||
"[bold]Remaining Size:[/bold] 0 KB",
|
||||
),
|
||||
]
|
||||
_render_output(data, fmt, rich_panels=panels, ok_message="Context updated")
|
||||
_render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=panels,
|
||||
ok_message="Context updated",
|
||||
command="agents actor context remove",
|
||||
)
|
||||
return
|
||||
|
||||
# Single context removal
|
||||
@@ -236,7 +247,13 @@ def context_remove(
|
||||
(f"[bold]Remaining Size:[/bold] {remaining_total} KB"),
|
||||
),
|
||||
]
|
||||
_render_output(data, fmt, rich_panels=panels, ok_message="Context updated")
|
||||
_render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=panels,
|
||||
ok_message="Context updated",
|
||||
command=f"agents actor context remove {name}",
|
||||
)
|
||||
|
||||
|
||||
@app.command("clear")
|
||||
@@ -377,7 +394,13 @@ def context_clear(
|
||||
),
|
||||
]
|
||||
|
||||
_render_output(data, fmt, rich_panels=panels, ok_message="Context cleared")
|
||||
_render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=panels,
|
||||
ok_message="Context cleared",
|
||||
command=f"agents actor context clear {context_label}",
|
||||
)
|
||||
|
||||
|
||||
@app.command("export")
|
||||
@@ -480,7 +503,13 @@ def context_export(
|
||||
(f"[bold]Checksum:[/bold] {checksum}\n[bold]Compressed:[/bold] no"),
|
||||
),
|
||||
]
|
||||
_render_output(data, fmt, rich_panels=panels, ok_message="Export completed")
|
||||
_render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=panels,
|
||||
ok_message="Export completed",
|
||||
command=f"agents actor context export {name}",
|
||||
)
|
||||
|
||||
|
||||
@app.command("import")
|
||||
@@ -600,4 +629,10 @@ def context_import(
|
||||
(f"[bold]Strategy:[/bold] {strategy}\n[bold]Conflicts:[/bold] 0"),
|
||||
),
|
||||
]
|
||||
_render_output(data, fmt, rich_panels=panels, ok_message="Import completed")
|
||||
_render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=panels,
|
||||
ok_message="Import completed",
|
||||
command=f"agents actor context import {resolved_name}",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
|
||||
from cleveragents.cli.formatting import OutputFormat
|
||||
from cleveragents.reactive.context_manager import ContextManager
|
||||
|
||||
|
||||
def _estimate_tokens_for_paths(paths: list[Path]) -> int:
|
||||
"""Return a coarse token estimate based on file sizes."""
|
||||
total = 0
|
||||
for path in paths:
|
||||
try:
|
||||
size = path.stat().st_size
|
||||
except OSError: # pragma: no cover
|
||||
continue
|
||||
tokens = size // 4
|
||||
total += tokens if tokens > 0 else 1
|
||||
return total
|
||||
|
||||
|
||||
def _format_metadata_timestamp(value: str | None) -> str | None:
|
||||
"""Return a human-readable timestamp for Rich output."""
|
||||
if not value: # pragma: no cover
|
||||
return None
|
||||
try:
|
||||
cleaned = value.replace("Z", "+00:00") if value.endswith("Z") else value
|
||||
dt = datetime.fromisoformat(cleaned)
|
||||
except (ValueError, TypeError): # pragma: no cover
|
||||
return value
|
||||
return dt.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def _default_context_base(context_dir: Path | None) -> Path:
|
||||
"""Return the base directory where named contexts are stored."""
|
||||
if context_dir is not None:
|
||||
return context_dir
|
||||
return Path.home() / ".cleveragents" / "context"
|
||||
|
||||
|
||||
def register_show_command(
|
||||
app: typer.Typer,
|
||||
render_output: Callable[..., None],
|
||||
format_help: str,
|
||||
) -> None:
|
||||
"""Register the `agents actor context show` command on *app*."""
|
||||
|
||||
# Note: we use the ``default=Option(...)`` pattern instead of
|
||||
# ``Annotated[str, Option(..., help=format_help)]`` because this module
|
||||
# imports ``from __future__ import annotations``: under PEP 563 typer's
|
||||
# ``inspect.signature(..., eval_str=True)`` would eval the annotation
|
||||
# string against module globals where the closure-captured ``format_help``
|
||||
# is not visible, raising ``NameError: name 'format_help' is not defined``.
|
||||
format_option = typer.Option(
|
||||
"rich",
|
||||
"--format",
|
||||
"-f",
|
||||
help=format_help,
|
||||
)
|
||||
|
||||
@app.command("show")
|
||||
def context_show(
|
||||
name: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Context name to display"),
|
||||
],
|
||||
context_dir: Annotated[
|
||||
Path | None,
|
||||
typer.Option(
|
||||
"--context-dir",
|
||||
help="Directory where contexts are stored",
|
||||
resolve_path=True,
|
||||
),
|
||||
] = None,
|
||||
fmt: str = format_option,
|
||||
) -> None:
|
||||
"""Show the content and metadata for a named actor context."""
|
||||
context_base = _default_context_base(context_dir)
|
||||
if not (context_base / name).exists():
|
||||
typer.echo(f"Error: Context '{name}' does not exist.", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
ctx_mgr = ContextManager(name, context_dir)
|
||||
files = [path for path in ctx_mgr.context_dir.rglob("*") if path.is_file()]
|
||||
total_size_kb = round(sum(p.stat().st_size for p in files) / 1024, 1)
|
||||
estimated_tokens = _estimate_tokens_for_paths(files)
|
||||
|
||||
created_at = ctx_mgr.metadata.get("created_at")
|
||||
created_human = _format_metadata_timestamp(created_at)
|
||||
|
||||
summary: dict[str, Any] = {
|
||||
"context": name,
|
||||
"files": len(files),
|
||||
"total_size_kb": total_size_kb,
|
||||
"estimated_tokens": estimated_tokens,
|
||||
"created": created_at,
|
||||
"last_updated": ctx_mgr.metadata.get("last_updated"),
|
||||
"message_count": len(ctx_mgr.messages),
|
||||
}
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"context_summary": summary,
|
||||
"messages": ctx_mgr.messages,
|
||||
"metadata": ctx_mgr.metadata,
|
||||
"state": ctx_mgr.state,
|
||||
"global_context": ctx_mgr.global_context,
|
||||
}
|
||||
|
||||
rich_panels: list[tuple[str, str]] | None = None
|
||||
if fmt == OutputFormat.RICH.value:
|
||||
summary_lines = [
|
||||
f"[bold]Context:[/bold] {name}",
|
||||
f"[bold]Files:[/bold] {len(files)}",
|
||||
f"[bold]Total Size:[/bold] {total_size_kb} KB",
|
||||
f"[bold]Estimated Tokens:[/bold] ~{estimated_tokens:,}",
|
||||
f"[bold]Messages:[/bold] {len(ctx_mgr.messages)}",
|
||||
]
|
||||
if created_human:
|
||||
summary_lines.append(f"[bold]Created:[/bold] {created_human}")
|
||||
elif created_at: # pragma: no cover
|
||||
summary_lines.append(f"[bold]Created:[/bold] {created_at}")
|
||||
last_updated = summary.get("last_updated")
|
||||
last_updated_human = _format_metadata_timestamp(last_updated)
|
||||
if last_updated_human:
|
||||
summary_lines.append(f"[bold]Last Updated:[/bold] {last_updated_human}")
|
||||
elif last_updated: # pragma: no cover
|
||||
summary_lines.append(f"[bold]Last Updated:[/bold] {last_updated}")
|
||||
|
||||
metadata_lines = [
|
||||
f"[bold]{key}:[/bold] {value}"
|
||||
for key, value in sorted(ctx_mgr.metadata.items())
|
||||
]
|
||||
|
||||
def _truncate_content(text: str, limit: int = 180) -> str:
|
||||
return text if len(text) <= limit else text[: limit - 3] + "..."
|
||||
|
||||
message_lines: list[str] = []
|
||||
max_messages = 10
|
||||
for idx, message in enumerate(ctx_mgr.messages, start=1):
|
||||
if idx > max_messages: # pragma: no cover
|
||||
remaining = len(ctx_mgr.messages) - max_messages
|
||||
msg_suffix = "s" if remaining != 1 else ""
|
||||
message_lines.append(
|
||||
f"(and {remaining} more message{msg_suffix}...)"
|
||||
)
|
||||
break
|
||||
role = message.get("role", "unknown")
|
||||
timestamp = message.get("timestamp", "")
|
||||
content = _truncate_content(message.get("content", ""))
|
||||
message_lines.append(
|
||||
f"[bold]{idx}. {role}[/bold] {timestamp}\n{content}"
|
||||
)
|
||||
|
||||
state_text = json.dumps(ctx_mgr.state, indent=2, default=str)
|
||||
global_context_text = json.dumps(
|
||||
ctx_mgr.global_context, indent=2, default=str
|
||||
)
|
||||
|
||||
rich_panels = [
|
||||
("Context Summary", "\n".join(summary_lines)),
|
||||
(
|
||||
"Metadata",
|
||||
"\n".join(metadata_lines) if metadata_lines else "(empty)",
|
||||
),
|
||||
]
|
||||
|
||||
if message_lines:
|
||||
rich_panels.append(("Messages", "\n\n".join(message_lines)))
|
||||
|
||||
if ctx_mgr.state: # pragma: no cover
|
||||
rich_panels.append(("State", state_text))
|
||||
|
||||
if ctx_mgr.global_context: # pragma: no cover
|
||||
rich_panels.append(("Global Context", global_context_text))
|
||||
|
||||
command_str = f"agents actor context show {name}"
|
||||
render_output(
|
||||
data,
|
||||
fmt,
|
||||
rich_panels=rich_panels,
|
||||
ok_message="Context displayed",
|
||||
command=command_str,
|
||||
)
|
||||
@@ -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.
|
||||
|
||||
@@ -97,31 +97,41 @@ class RetryPolicyConfig(BaseModel):
|
||||
override exists.
|
||||
|
||||
Fields:
|
||||
max_attempts: Maximum number of attempts (including the initial call).
|
||||
base_delay: Initial delay in seconds before the first retry.
|
||||
max_delay: Upper bound on delay between retries in seconds.
|
||||
max_retries: Maximum number of retry attempts (spec-required name).
|
||||
retry_delay_seconds: Initial delay in seconds before the first retry.
|
||||
backoff_factor: Multiplicative factor applied between retry delays.
|
||||
max_backoff: Upper bound on backoff delay in seconds (spec-required name).
|
||||
jitter: Whether to add random jitter to delays to avoid thundering herd.
|
||||
backoff_strategy: The strategy used to compute delay between retries.
|
||||
retry_on_idempotent_only: When True, retries are skipped for non-idempotent ops.
|
||||
"""
|
||||
|
||||
max_attempts: int = Field(
|
||||
max_retries: int = Field(
|
||||
default=3,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Maximum number of attempts including the initial call.",
|
||||
description="Maximum number of retry attempts.",
|
||||
)
|
||||
base_delay: float = Field(
|
||||
retry_delay_seconds: float = Field(
|
||||
default=1.0,
|
||||
ge=0.0,
|
||||
le=300.0,
|
||||
description="Initial delay in seconds before the first retry.",
|
||||
)
|
||||
max_delay: float = Field(
|
||||
backoff_factor: float = Field(
|
||||
default=2.0,
|
||||
ge=1.0,
|
||||
le=10.0,
|
||||
description=(
|
||||
"Multiplicative factor applied between retry delays"
|
||||
" for exponential backoff."
|
||||
),
|
||||
)
|
||||
max_backoff: float = Field(
|
||||
default=60.0,
|
||||
ge=0.0,
|
||||
le=3600.0,
|
||||
description="Upper bound on delay between retries in seconds.",
|
||||
description="Upper bound on backoff delay in seconds.",
|
||||
)
|
||||
jitter: bool = Field(
|
||||
default=True,
|
||||
@@ -148,17 +158,17 @@ class RetryPolicyConfig(BaseModel):
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _check_max_delay_ge_base_delay(self) -> RetryPolicyConfig:
|
||||
"""Ensure max_delay is not less than base_delay.
|
||||
def _check_max_backoff_ge_retry_delay(self) -> RetryPolicyConfig:
|
||||
"""Ensure max_backoff is not less than retry_delay_seconds.
|
||||
|
||||
Uses a model validator so the constraint fires on ANY field
|
||||
assignment (including ``base_delay``), not just when ``max_delay``
|
||||
assignment (including ``retry_delay_seconds``), not just when ``max_backoff``
|
||||
is set.
|
||||
"""
|
||||
if self.max_delay < self.base_delay:
|
||||
if self.max_backoff < self.retry_delay_seconds:
|
||||
msg = (
|
||||
f"max_delay ({self.max_delay}) must be >= "
|
||||
f"base_delay ({self.base_delay})"
|
||||
f"max_backoff ({self.max_backoff}) must be >= "
|
||||
f"retry_delay_seconds ({self.retry_delay_seconds})"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
return self
|
||||
@@ -300,36 +310,40 @@ class ServiceRetryPolicy(BaseModel):
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
DEFAULT_NETWORK_RETRY = RetryPolicyConfig(
|
||||
max_attempts=5,
|
||||
base_delay=1.0,
|
||||
max_delay=30.0,
|
||||
max_retries=5,
|
||||
retry_delay_seconds=1.0,
|
||||
backoff_factor=2.0,
|
||||
max_backoff=30.0,
|
||||
jitter=True,
|
||||
backoff_strategy=RetryStrategy.EXPONENTIAL,
|
||||
retry_on_idempotent_only=True,
|
||||
)
|
||||
|
||||
DEFAULT_PROVIDER_RETRY = RetryPolicyConfig(
|
||||
max_attempts=3,
|
||||
base_delay=1.0,
|
||||
max_delay=60.0,
|
||||
max_retries=3,
|
||||
retry_delay_seconds=1.0,
|
||||
backoff_factor=2.0,
|
||||
max_backoff=60.0,
|
||||
jitter=True,
|
||||
backoff_strategy=RetryStrategy.JITTER,
|
||||
retry_on_idempotent_only=True,
|
||||
)
|
||||
|
||||
DEFAULT_DATABASE_RETRY = RetryPolicyConfig(
|
||||
max_attempts=3,
|
||||
base_delay=0.5,
|
||||
max_delay=5.0,
|
||||
max_retries=3,
|
||||
retry_delay_seconds=0.5,
|
||||
backoff_factor=2.0,
|
||||
max_backoff=5.0,
|
||||
jitter=False,
|
||||
backoff_strategy=RetryStrategy.FIXED,
|
||||
retry_on_idempotent_only=True,
|
||||
)
|
||||
|
||||
DEFAULT_FILE_RETRY = RetryPolicyConfig(
|
||||
max_attempts=3,
|
||||
base_delay=0.1,
|
||||
max_delay=1.0,
|
||||
max_retries=3,
|
||||
retry_delay_seconds=0.1,
|
||||
backoff_factor=2.0,
|
||||
max_backoff=1.0,
|
||||
jitter=True,
|
||||
backoff_strategy=RetryStrategy.EXPONENTIAL,
|
||||
retry_on_idempotent_only=True,
|
||||
@@ -450,7 +464,7 @@ class ServiceRetryPolicyRegistry:
|
||||
registry = ServiceRetryPolicyRegistry()
|
||||
policy = registry.get("plan_service")
|
||||
# Apply override from config
|
||||
registry.apply_overrides({"plan_service": {"retry": {"max_attempts": 5}}})
|
||||
registry.apply_overrides({"plan_service": {"retry": {"max_retries": 5}}})
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
Reference in New Issue
Block a user