Compare commits

...

9 Commits

Author SHA1 Message Date
CleverAgents Bot bb87e81acd ci: stop master workflow on PR updates
CI / lint (pull_request) Successful in 53s
CI / push-validation (pull_request) Successful in 30s
CI / typecheck (pull_request) Successful in 1m1s
CI / quality (pull_request) Successful in 57s
CI / helm (pull_request) Successful in 44s
CI / build (pull_request) Successful in 47s
CI / security (pull_request) Successful in 1m21s
CI / integration_tests (pull_request) Failing after 3m14s
CI / e2e_tests (pull_request) Failing after 3m24s
CI / unit_tests (pull_request) Failing after 4m18s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 6s
Remove the stale pull_request trigger from master.yml so PR branch commits do not launch the master workflow.

Maintenance patch for PR #11109.
2026-06-10 20:17:48 -04:00
HAL9000 7bef6ba706 fix(devcontainer): remove invalid trailing commas from opencode.json
CI / lint (pull_request) Successful in 1m12s
CI / build (pull_request) Successful in 1m6s
CI / quality (pull_request) Successful in 1m21s
CI / typecheck (pull_request) Successful in 1m35s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 1m55s
CI / push-validation (pull_request) Successful in 39s
CI / helm (pull_request) Successful in 42s
CI / benchmark-regression (pull_request) Failing after 1m14s
CI / integration_tests (pull_request) Successful in 4m8s
CI / e2e_tests (pull_request) Successful in 5m24s
CI / unit_tests (pull_request) Successful in 6m27s
CI / docker (pull_request) Successful in 1m37s
CI / coverage (pull_request) Successful in 12m34s
CI / status-check (pull_request) Successful in 3s
The JSON config file contained multiple trailing commas which caused
Python's strict JSON parser to fail. Fixed all occurrences where
commas appeared after the last property value in objects before closing braces.
2026-05-11 00:39:49 +00:00
CoreRasurae 78be08870c fix(cli): validate actor provider field at correct config nesting level
CI / lint (push) Successful in 1m10s
CI / quality (push) Successful in 1m16s
CI / build (push) Successful in 1m1s
CI / benchmark-regression (push) Has been skipped
CI / typecheck (push) Successful in 1m46s
CI / security (push) Successful in 1m58s
CI / push-validation (push) Successful in 43s
CI / helm (push) Successful in 47s
CI / e2e_tests (push) Successful in 4m9s
CI / integration_tests (push) Successful in 5m9s
CI / unit_tests (push) Successful in 7m45s
CI / docker (push) Successful in 1m49s
CI / coverage (push) Successful in 14m18s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h31m39s
CI / lint (pull_request) Successful in 1m17s
CI / typecheck (pull_request) Successful in 1m30s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 1m33s
CI / push-validation (pull_request) Successful in 48s
CI / helm (pull_request) Successful in 49s
CI / build (pull_request) Successful in 1m32s
CI / benchmark-regression (pull_request) Failing after 2m1s
CI / quality (pull_request) Successful in 2m9s
CI / e2e_tests (pull_request) Successful in 4m58s
CI / integration_tests (pull_request) Successful in 5m23s
CI / unit_tests (pull_request) Successful in 6m43s
CI / docker (pull_request) Successful in 1m36s
CI / coverage (pull_request) Successful in 10m58s
CI / status-check (pull_request) Successful in 5s
- Actor configuration in V3 is now obtained from the nested configuration parameter, according to the specification.
- Removed the legacy V2 fallback support and the tests affected by that removal. Mocked existing steps to allow remaining V2 features to be covered/tested.
- Fix add() to pass unsafe flag to add_v3() for proper unsafe actor handling
- Add _extract_nested_v3_config() to extract provider/model from nested
  actors.<name>.config block before v3 schema validation
- Remove v2 extraction test scenarios from consolidated_actor.feature
  that call removed _extract_v2_actor() and _extract_v2_options() methods
- Update test YAML in actor_registry_spec_yaml_steps.py to exercise nested type detection (no top-level type field)
- Add @tdd_issue_4300 regression test tag to existing spec-compliant
  actors map scenario

ISSUES CLOSED: #4300
2026-05-09 23:13:33 +01:00
HAL9000 5ee08ea946 fix(plan): remove TDD expected fail tag and fix format violations
CI / benchmark-regression (push) Has been skipped
CI / lint (push) Successful in 1m21s
CI / quality (push) Successful in 1m33s
CI / helm (push) Successful in 51s
CI / build (push) Successful in 1m12s
CI / push-validation (push) Successful in 50s
CI / security (push) Successful in 1m59s
CI / typecheck (push) Successful in 2m18s
CI / integration_tests (push) Successful in 4m51s
CI / e2e_tests (push) Successful in 5m1s
CI / unit_tests (push) Successful in 6m42s
CI / docker (push) Successful in 1m36s
CI / coverage (push) Successful in 13m8s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h25m1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m12s
CI / typecheck (pull_request) Successful in 1m36s
CI / docker (pull_request) Successful in 1m28s
CI / integration_tests (pull_request) Successful in 4m52s
CI / push-validation (pull_request) Successful in 20s
CI / quality (pull_request) Successful in 1m23s
CI / helm (pull_request) Successful in 36s
CI / build (pull_request) Successful in 58s
CI / lint (pull_request) Successful in 1m6s
CI / security (pull_request) Successful in 1m47s
CI / e2e_tests (pull_request) Successful in 4m21s
CI / unit_tests (pull_request) Successful in 5m51s
CI / coverage (pull_request) Successful in 12m1s
CI / status-check (pull_request) Successful in 4s
- Remove @tdd_expected_fail from Tree with json format scenario in
  plan_explain.feature (bug is now fixed by the envelope implementation)
- Update scenario assertions to verify new spec-required envelope format
  instead of old bare-array format
- Add step_tree_json_valid_envelope step definition in plan_explain_steps.py
- Fix ruff format violation in plan_explain_cli_coverage_steps.py

ISSUES CLOSED: #9163
2026-05-09 12:23:24 +00:00
HAL9000 6e1646d565 fix(plan): wrap plan tree JSON/YAML output in spec-required command envelope.\n\nISSUES CLOSED: #9163
# Conflicts:
#	CONTRIBUTORS.md
2026-05-09 12:23:24 +00:00
HAL9000 815f546bd2 fix(plan): wrap plan tree JSON/YAML output in spec-required command envelope. 2026-05-09 12:23:24 +00:00
HAL9000 f78c1c2c98 docs(resources): correct CHANGELOG entry to accurately describe DatabaseResourceHandler
CI / benchmark-regression (push) Has been skipped
CI / push-validation (push) Successful in 45s
CI / helm (push) Successful in 1m4s
CI / build (push) Successful in 1m13s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m25s
CI / security (push) Successful in 1m54s
CI / typecheck (push) Successful in 1m55s
CI / e2e_tests (push) Successful in 4m20s
CI / integration_tests (push) Successful in 4m26s
CI / unit_tests (push) Successful in 6m50s
CI / docker (push) Successful in 1m33s
CI / coverage (push) Successful in 15m36s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h31m44s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m35s
CI / typecheck (pull_request) Successful in 1m44s
CI / security (pull_request) Successful in 1m52s
CI / benchmark-regression (pull_request) Failing after 1m6s
CI / helm (pull_request) Successful in 44s
CI / push-validation (pull_request) Successful in 52s
CI / build (pull_request) Successful in 1m23s
CI / integration_tests (pull_request) Successful in 4m22s
CI / e2e_tests (pull_request) Successful in 4m31s
CI / unit_tests (pull_request) Successful in 6m42s
CI / docker (pull_request) Successful in 1m59s
CI / coverage (pull_request) Successful in 13m25s
CI / status-check (pull_request) Successful in 4s
Replace the factual inaccuracy claiming a PostgreSQL resource subclass
with accurate description of the unified DatabaseResourceHandler design.

The PR description and all code use a unified handler pattern with type-specific
routing via PostgreSQL, MySQL, SQLite, and DuckDB type definitions - there is
no PostgreSQLResource subclass anywhere in the codebase. This fix was requested
in review #8172.
2026-05-09 10:01:20 +00:00
HAL9000 3f0ce3d20a feat(resources): add CHANGELOG and CONTRIBUTORS entries for PR #10591
- Add CHANGELOG.md entry under [Unreleased] / Database resource types
  (PostgreSQL, SQLite) with transaction-based sandbox strategy
  (issues #8608, Epic #8568)
- Update CONTRIBUTORS.md with HAL 9000 contribution note for database
  resource types implementation

ISSUES CLOSED: #8608
2026-05-09 10:01:20 +00:00
HAL9000 2cba7d41bc chore(agents): fix bug-hunt-pool-supervisor tracking prefix AUTO-BUG-POOL to AUTO-BUG-SUP (complete fix)
CI / coverage (push) Blocked by required conditions
CI / docker (push) Blocked by required conditions
CI / status-check (push) Blocked by required conditions
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 48s
CI / push-validation (push) Successful in 35s
CI / build (push) Successful in 1m4s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m22s
CI / typecheck (push) Successful in 1m46s
CI / e2e_tests (push) Successful in 4m42s
CI / integration_tests (push) Successful in 7m51s
CI / unit_tests (push) Successful in 12m20s
CI / security (push) Failing after 13m26s
CI / benchmark-publish (push) Successful in 1h31m13s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m27s
CI / status-check (pull_request) Successful in 3s
CI / integration_tests (pull_request) Successful in 4m32s
CI / unit_tests (pull_request) Successful in 9m46s
CI / helm (pull_request) Successful in 54s
CI / build (pull_request) Successful in 1m4s
CI / push-validation (pull_request) Successful in 22s
CI / docker (pull_request) Successful in 1m48s
CI / lint (pull_request) Successful in 1m10s
CI / quality (pull_request) Successful in 1m27s
CI / typecheck (pull_request) Successful in 1m42s
CI / e2e_tests (pull_request) Successful in 5m24s
CI / security (pull_request) Successful in 2m10s
CI / coverage (pull_request) Successful in 12m19s
Replace all remaining AUTO-BUG-POOL references with correct AUTO-BUG-SUP in agent-system-specification.md, automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).

ISSUES CLOSED: #7875
2026-05-09 00:05:17 +00:00
25 changed files with 691 additions and 1688 deletions
+9 -9
View File
@@ -11,7 +11,7 @@
"type": "local",
"command": ["npx", "@modelcontextprotocol/server-sequential-thinking"],
"enabled": false
},
}
},
"provider": {
"CleverThis": {
@@ -21,7 +21,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -38,7 +38,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -59,7 +59,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -76,7 +76,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -93,7 +93,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -110,7 +110,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -127,7 +127,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
@@ -144,7 +144,7 @@
"apiKey": "{env:HF_TOKEN}",
"headers": {
"X-HF-Bill-To": "CleverThis",
"Authorization": "Bearer {env:HF_TOKEN}",
"Authorization": "Bearer {env:HF_TOKEN}"
}
},
"models": {
-2
View File
@@ -3,8 +3,6 @@ name: CI
on:
push:
branches: [master, develop]
pull_request:
branches: [master, develop]
vars:
docker_prefix: "http://harbor.cleverthis.com/docker/"
+35
View File
@@ -14,6 +14,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
from the TDD test so both scenarios run as normal regression guards. (#988)
### Fixed
- **Actor configuration validation incorrectly requires top-level provider field** (#4300):
Actor configuration in V3 is now obtained from the nested configuration
parameter, according to the specification.
Removed the legacy V2 fallback support and the tests affected by that
removal. Mocked existing steps to allow remaining V2 features to be
covered/tested.
- **TUI Prompt Symbol Mode Awareness** (#6431): The prompt widget now displays a
mode-dependent symbol (`` normal, `/` command, `$` shell, `☰` multi-line),
implemented via `_PromptSymbolMixin` and `InputMode.MULTILINE`. The widget uses
@@ -96,6 +103,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- Fixed stale `AUTO-BUG-POOL` tracking prefix references in automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).
- **`agents session list` now displays full 26-character session ULIDs** (#10970): The Rich table
and Summary panel ("Most Recent" / "Oldest") previously showed only the first 8 characters of
each session ULID. This made the output unusable for copy-paste into `session tell`,
@@ -220,6 +229,25 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
failure paths. Comprehensive BDD test coverage validates the fix under concurrent
execution and confirms proper cleanup behavior.
- **Database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy** (#8608):
Implemented comprehensive database resource support enabling users to interact with
PostgreSQL and SQLite backends through a unified resource interface. Introduces
`DatabaseResourceHandler` providing full CRUD operations (`read`, `write`, `delete`,
`list_children`), connection validation with automatic credential masking via
:mod:`cleveragents.shared.redaction`, and transaction-based sandbox strategy using
BEGIN/COMMIT/ROLLBACK wrappers for safe, isolated database operations. SQLite-specific
checkpoint and rollback support with SAVEPOINT semantics. Support for multiple backends (PostgreSQL, SQLite, MySQL, DuckDB) via unified "DatabaseResourceHandler" and type-specific routing. BDD test
coverage in ``features/database_resources.feature`` (connection validation, CRUD workflows,
transaction/rollback behavior, error handling, credential masking verification) and
Robot Framework integration tests in ``robot/database_resources.robot``.
- **TransactionSandbox infrastructure for database resource isolation** (#8608):
Implemented ``TransactionSandbox`` class with BEGIN/COMMIT/ROLLBACK lifecycle
management for transaction-based sandbox strategy. Wired into ``SandboxFactory``
as the strategy resolver for database resource types. Added ``database`` resource type
registration in bootstrap builtin types and updated ``_resource_registry_data.py``
to recognize database resource categories.
### Fixed
- **fix(repositories): derive PlanResult.success from result_success column instead of error_message** (#7501):
@@ -420,6 +448,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
MCP logger thread-safety in `session.py` using a threading lock. Created
migration guide for users transitioning from legacy to V3 workflow.
- **Plan Tree JSON/YAML Command Envelope** (#9163): `agents plan tree --format json/yaml`
now wraps output in the spec-required command envelope with `command`, `status`,
`exit_code`, `data`, `timing`, and `messages` fields. The `data` field contains
`plan_id`, `tree`, `summary` (nodes, depth, child_plans, invariants, superseded),
`child_plans` list, and `decision_ids` mapping. Timing now reflects actual elapsed
milliseconds from command start to envelope construction.
- **Automation Profile Silent Fallback** (#8232): `_resolve_profile_for_plan` in
`PlanLifecycleService` now raises a clear `ValidationError` when a plan's
automation profile name is not a known built-in profile, instead of silently
+5 -4
View File
@@ -17,12 +17,12 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix: updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
<<<<<<< HEAD
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix (#7875 / PR #7957): updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
* Jeffrey Phillips Freeman has contributed the complete AUTO-BUG-POOL to AUTO-BUG-SUP tracking prefix fix across agent-system-specification.md, automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
* HAL 9000 has contributed the agent-evolution-pool-supervisor PR metadata assignment (#7888): the supervisor now automatically looks up the Type/Automation label and earliest open milestone before dispatching improvement PR creation workers, ensuring all generated improvement PRs have correct Type labels and milestone assignments.
* 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 automated specification maintenance, documentation updates, and bot-driven PR authorship.
* HAL 9000 has contributed the plan tree JSON/YAML command envelope fix (#9163): wrapped `agents plan tree --format json/yaml` output in the spec-required command envelope structure, added summary statistics, decision_ids mapping, child_plans list, and accurate timing measurement.
* 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 file edit encoding parameter fix (PR #8258 / issue #7559).
@@ -38,3 +38,4 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568).
@@ -665,7 +665,7 @@ The Automation Tracking Manager provides eleven operations:
| `AUTO-IMP-POOL` | Implementation Pool Supervisor |
| `AUTO-REV-POOL` | PR Review Pool Supervisor |
| `AUTO-UAT-POOL` | UAT Test Pool Supervisor |
| `AUTO-BUG-POOL` | Bug Hunt Pool Supervisor |
| `AUTO-BUG-SUP` | Bug Hunt Pool Supervisor |
| `AUTO-INF-POOL` | Test Infrastructure Pool Supervisor |
| `AUTO-ARCH` | Architecture Supervisor |
| `AUTO-EPIC` | Epic Planning Supervisor |
@@ -693,7 +693,7 @@ The following table maps between the two schemes for agents where they differ:
| Implementation Pool | `AUTO-IMP-SUP` | `AUTO-IMP-POOL` |
| PR Review Pool | `AUTO-REV-SUP` | `AUTO-REV-POOL` |
| UAT Test Pool | `AUTO-UAT-SUP` | `AUTO-UAT-POOL` |
| Bug Hunt Pool | `AUTO-BUG-SUP` | `AUTO-BUG-POOL` |
| Bug Hunt Pool | `AUTO-BUG-SUP` | `AUTO-BUG-SUP` |
| Test Infrastructure Pool | `AUTO-INF-SUP` | `AUTO-INF-POOL` |
| Human Liaison | `AUTO-HUMAN` | `AUTO-LIAISON` |
| Grooming | `AUTO-GROOM` | `AUTO-GROOMER` |
@@ -1869,7 +1869,7 @@ Only critical bugs get assigned to the active milestone. Non-critical issues go
| **Mode** | `subagent` |
| **Model** | `google/gemini-2.5-pro` |
| **Temperature** | 0.1 |
| **Tracking Prefix** | `AUTO-BUG-POOL` |
| **Tracking Prefix** | `AUTO-BUG-SUP` |
| **Worker Count** | N/4 (quarter allocation) |
#### 8.6.1 Purpose
@@ -3164,7 +3164,7 @@ Defines how ALL agents discover, interact with, and coordinate through the autom
### 13.9 Bug Hunter Tracking Update (`shared/bug_hunter_tracking_update.md`)
Defines the migration guide for converting the Bug Hunt supervisor from the old session state system to the new individual tracking issue system. Documents the tracking issue format `[AUTO-BUG-POOL] Bug Detection Pool Status (Cycle N)`, the cleanup protocol (one issue per cycle, preserve announcements), and worker coordination via announcement issues.
Defines the migration guide for converting the Bug Hunt supervisor from the old session state system to the new individual tracking issue system. Documents the tracking issue format `[AUTO-BUG-SUP] Bug Hunt Status (Cycle 25)`, the cleanup protocol (one issue per cycle, preserve announcements), and worker coordination via announcement issues.
---
@@ -6767,7 +6767,7 @@ The system uses five distinct redundancy patterns:
| 4 | pr-merge-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-MERGE | Singleton Supervisor |
| 5 | pr-fix-pool-supervisor | *(removed — absorbed into implementation-pool-supervisor)* | -- | -- | -- |
| 6 | uat-test-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-UAT-POOL | Pool Supervisor |
| 7 | bug-hunt-pool-supervisor | subagent | gemini-2.5-pro | AUTO-BUG-POOL | Pool Supervisor |
| 7 | bug-hunt-pool-supervisor | subagent | gemini-2.5-pro | AUTO-BUG-SUP | Pool Supervisor |
| 8 | test-infra-pool-supervisor | subagent | gemini-2.5-pro | AUTO-INF-POOL | Pool Supervisor |
| 9 | architecture-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-ARCH | Singleton Supervisor |
| 10 | epic-planning-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-EPIC | Singleton Supervisor |
+2 -2
View File
@@ -61,7 +61,7 @@ All prefixes are registered in the `automation-tracking-manager` subagent, which
| uat-test-pool-supervisor | `AUTO-UAT-POOL` | `[AUTO-UAT-POOL] UAT Status (Cycle 6)` |
| project-owner-pool-supervisor | `AUTO-PROJ-OWN` | `[AUTO-PROJ-OWN] Project Status (Cycle 11)` |
| agent-evolution-pool-supervisor | `AUTO-EVLV` | `[AUTO-EVLV] Agent Evolution Report (Cycle 10)` |
| bug-hunt-pool-supervisor | `AUTO-BUG-POOL` | `[AUTO-BUG-POOL] Bug Detection Pool Status (Cycle 60)` |
| bug-hunt-pool-supervisor | `AUTO-BUG-SUP` | `[AUTO-BUG-SUP] Bug Hunt Status (Cycle 25)` |
| spec-update-pool-supervisor | `AUTO-SPEC` | `[AUTO-SPEC] Specification Update Report (Cycle 1)` |
| test-infra-pool-supervisor | `AUTO-INF-POOL` | `[AUTO-INF-POOL] Infrastructure Analysis Report (Cycle 2)` |
| epic-planner | `AUTO-EPIC` | `[AUTO-EPIC] Epic Planning Update (Cycle 9)` |
@@ -394,7 +394,7 @@ label:"Automation Tracking" [AUTO-UAT-POOL] in:title
label:"Automation Tracking" [AUTO-PROJ-OWN] in:title
label:"Automation Tracking" [AUTO-EVLV] in:title
label:"Automation Tracking" [AUTO-EPIC] in:title
label:"Automation Tracking" [AUTO-BUG-POOL] in:title
label:"Automation Tracking" [AUTO-BUG-SUP] in:title
label:"Automation Tracking" [AUTO-SPEC] in:title
label:"Automation Tracking" [AUTO-INF-POOL] in:title
```
-130
View File
@@ -279,140 +279,10 @@ Feature: Actor configuration uncovered lines coverage
When I build an actor configuration from blob {"provider": "only-provider"}
Then a ValueError should be raised containing "model is required"
Scenario: from_blob merges default and v2 and override options
When I build an actor configuration from structured blob with defaults and overrides:
"""
{
"blob": {
"provider": "cli-provider",
"model": "cli-model",
"options": {"user": "blob"},
"agents": {
"writer": {
"config": {
"options": {"v2": "v2-option"}
}
}
}
},
"default_options": {"base": "default"},
"option_overrides": {"user": "override", "extra": "added"}
}
"""
Then the actor configuration should have provider "cli-provider" and model "cli-model"
And the actor configuration options should equal {"base": "default", "v2": "v2-option", "user": "override", "extra": "added"}
Scenario: from_blob with None blob defaults to empty data
When I build actor config from None blob with provider and model overrides
Then the actor configuration should have provider "override-provider" and model "override-model"
# --- _extract_v2_actor: lines 275-307 ---
Scenario: v2 YAML actor config infers provider and model
Given an actor config file "v2.yaml" with content:
"""
cleveragents:
default_router: main_router
agents:
paper_writer:
type: llm
config:
provider: openai
model: gpt-4
unsafe: true
options:
temperature: 0.5
routes:
main_router:
type: stream
operators:
- type: map
params:
agent: paper_writer
publications:
- __output__
"""
When I parse the actor configuration from file "v2.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration unsafe flag should be true
And the actor configuration graph descriptor should include key "routes"
And the actor configuration graph descriptor should include key "agents"
And the actor configuration graph descriptor should include key "cleveragents"
And the actor configuration options should equal {"temperature": 0.5}
Scenario: v2 extraction includes merges and templates keys when present
Given an actor config file "v2_merges.yaml" with content:
"""
agents:
writer:
config:
provider: openai
model: gpt-4
merges:
combined:
type: join
templates:
default:
system_prompt: "hello"
"""
When I parse the actor configuration from file "v2_merges.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration graph descriptor should include key "merges"
And the actor configuration graph descriptor should include key "templates"
Scenario: v2 extraction handles agent entry without config block
Given an actor config file "v2_no_config.yaml" with content:
"""
provider: fallback-provider
model: fallback-model
agents:
first:
type: llm
"""
When I parse the actor configuration from file "v2_no_config.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "fallback-provider" and model "fallback-model"
# --- _extract_v2_options: lines 316-326 ---
Scenario: v2 extraction skips non-dict agent entries
Given an actor config file "invalid_v2.yaml" with content:
"""
provider: fallback-provider
model: fallback-model
agents:
first: not-a-dict
"""
When I parse the actor configuration from file "invalid_v2.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "fallback-provider" and model "fallback-model"
Scenario: v2 options extraction returns None for agents without options
Given an actor config file "v2_no_opts.yaml" with content:
"""
agents:
writer:
config:
provider: openai
model: gpt-4
"""
When I parse the actor configuration from file "v2_no_opts.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration options should equal {}
Scenario: from_blob reads provider_type and model_id aliases
When I build an actor configuration from blob {"provider_type": "aliased-provider", "model_id": "aliased-model"}
Then the actor configuration should have provider "aliased-provider" and model "aliased-model"
+3 -399
View File
@@ -15,6 +15,7 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
And the registered actor name should be "local/my-assistant"
And the registered actor should exist in the actor service
@tdd_issue @tdd_issue_4300
Scenario: registry.add() accepts spec-compliant actors: map with separate provider and model
When I add a spec-compliant YAML with actors map and separate provider model
Then the actor should be registered with provider "anthropic" and model "claude-3"
@@ -53,24 +54,13 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
Then the actor should be registered with provider "nested-provider" and model "top-level-model"
And the registered actor should exist in the actor service
# ── Graph descriptor preserved from nested config ──────────────────
Scenario: registry.add() preserves graph descriptor from nested spec-compliant config
When I add a spec-compliant YAML with actors map and combined actor field
Then the registered actor graph descriptor should contain key "actors"
# ── Top-level provider+model still picks up nested unsafe/graph ──────
# ── Top-level provider+model still picks up nested unsafe ───────────
Scenario: registry.add() with top-level provider and model detects nested unsafe flag
When I add a YAML with top-level provider and model and nested actors map with unsafe flag
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
Scenario: registry.add() with top-level provider and model detects nested graph descriptor
When I add a YAML with top-level provider and model and nested actors map with graph descriptor
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "actors"
# ── Unsafe confirmation gate ───────────────────────────────────────
Scenario: registry.add() rejects unsafe actor without confirmation
@@ -94,12 +84,6 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
# ── Missing provider/model still rejected for non-v3 YAML ─────────
Scenario: registry.add() rejects non-v3 YAML without any provider or model
When I attempt to add a YAML with no provider or model anywhere
Then a spec-yaml ValidationError should be raised containing "provider"
# ── update=True path ───────────────────────────────────────────────
Scenario: registry.add() with update=True overwrites an existing actor using spec-compliant YAML
@@ -139,366 +123,6 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
# ── Multi-actor YAML rejection ───────────────────────────────────
Scenario: registry.add() rejects multi-actor YAML with a ValidationError
When I attempt to add a multi-actor YAML with two actor entries
Then a spec-yaml ValidationError should be raised containing "single-actor"
Scenario: registry.add() rejects multi-actor YAML via agents: fallback when actors: is null
When I attempt to add a YAML with actors null and multi-entry agents map
Then a spec-yaml ValidationError should be raised containing "single-actor"
# ── _extract_v2_actor handles actors: key ──────────────────────────
Scenario: _extract_v2_actor extracts provider/model from actors: map
When I call _extract_v2_actor with an actors map containing combined actor field
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor extracts from actors: map with separate fields
When I call _extract_v2_actor with an actors map containing separate provider model
Then the spec-yaml extracted provider should be "anthropic"
And the spec-yaml extracted model should be "claude-3"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor prefers actors: key over agents: key
When I call _extract_v2_actor with both actors and agents maps
Then the spec-yaml extracted provider should be "actors-provider"
And the spec-yaml extracted model should be "actors-model"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor graph descriptor contains agent key matching the actor entry name
When I call _extract_v2_actor with an actors map containing combined actor field
Then the spec-yaml extracted graph descriptor should contain key "agent"
And the spec-yaml extracted graph descriptor agent value should be "my_assistant"
Scenario: _extract_v2_actor with actors map containing unsafe flag
When I call _extract_v2_actor with an actors map containing unsafe true
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor returns None for empty data
When I call _extract_v2_actor with an empty dict
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
Scenario: _extract_v2_actor returns None for actors key with empty map
When I call _extract_v2_actor with actors key containing empty map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
Scenario: _extract_v2_actor returns None for actors key with None value
When I call _extract_v2_actor with actors key containing None value
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
# ── _extract_v2_actor handles agents: key ─────────────────────────
Scenario: _extract_v2_actor returns graph descriptor with agents map_key
When I call _extract_v2_actor with an agents map containing combined actor field
Then the spec-yaml extracted graph descriptor should contain key "agents"
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge cases: non-dict / missing config ────────
Scenario: _extract_v2_actor returns None for non-dict first entry
When I call _extract_v2_actor with a non-dict first entry in actors map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
Scenario: _extract_v2_actor returns None for dict entry missing config block
When I call _extract_v2_actor with a dict entry missing config block
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge case: actors: {} blocks agents: fallback ─
Scenario: _extract_v2_actor with empty actors dict blocks agents fallback
When I call _extract_v2_actor with empty actors dict and valid agents map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge case: actors: [] (list type) ────────────
Scenario: _extract_v2_actor with actors as list returns None
When I call _extract_v2_actor with actors as a list
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_options handles actors: and agents: keys ───────────
Scenario: _extract_v2_options extracts options from actors: map
When I call _extract_v2_options with an actors map containing options
Then the spec-yaml extracted options should contain key "temperature" with value 0.7
Scenario: _extract_v2_options extracts options from agents: map
When I call _extract_v2_options with an agents map containing options
Then the spec-yaml extracted options should contain key "max_tokens" with value 1024
Scenario: _extract_v2_options returns None for actors key with empty map
When I call _extract_v2_options with actors key containing empty map
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for actors key with None value
When I call _extract_v2_options with actors key containing None value
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for actors key with list value
When I call _extract_v2_options with actors key containing list value
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None when config block has no options key
When I call _extract_v2_options with an actors map where config has no options key
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for non-dict first entry in actors map
When I call _extract_v2_options with a non-dict first entry in actors map
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for dict entry missing config block
When I call _extract_v2_options with a dict entry missing config block
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options prefers actors: key over agents: key
When I call _extract_v2_options with both actors and agents maps containing options
Then the spec-yaml extracted options should contain key "source" with value "actors"
# ── Unsafe coercion edge cases (unsafe coercion) ────────────────
Scenario: _extract_v2_actor treats unsafe: "no" as False (not truthy string)
When I call _extract_v2_actor with unsafe value "no"
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: "yes" as False (not truthy string)
When I call _extract_v2_actor with unsafe value "yes"
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 1 (integer) as True
When I call _extract_v2_actor with unsafe value 1
Then the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: registry.add() accepts actors map with unsafe: 1 (integer) and unsafe flag
When I add a YAML with actors map where unsafe is integer 1 and the unsafe flag set
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
Scenario: _extract_v2_actor treats unsafe: 1.0 (float) as True
When I call _extract_v2_actor with unsafe value 1.0
Then the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 2 (integer > 1) as False
When I call _extract_v2_actor with unsafe value 2
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 0 (integer zero) as False
When I call _extract_v2_actor with unsafe value 0
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
# ── Top-level unsafe: 1 (integer) through registry.add() ────────────
Scenario: registry.add() rejects top-level unsafe: 1 (integer) without confirmation
When I attempt to add a YAML with top-level unsafe integer 1 and no flag
Then a spec-yaml ValidationError should be raised containing "unsafe"
Scenario: registry.add() accepts top-level unsafe: 1 (integer) with unsafe flag
When I add a YAML with top-level unsafe integer 1 and the unsafe flag set
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
# ── Top-level graph_descriptor key through registry.add() (T7) ──────
Scenario: registry.add() resolves graph descriptor from top-level graph_descriptor key
When I add a YAML with top-level provider model and graph_descriptor key
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "workflow"
Scenario: _extract_v2_actor includes top-level routes key in graph descriptor
When I call _extract_v2_actor with an actors map and a top-level routes key
Then the spec-yaml extracted graph descriptor should contain key "routes"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Legacy graph key fallback (M3) ─────────────────────────────────
Scenario: registry.add() resolves graph descriptor from legacy top-level graph key
When I add a YAML with top-level provider model and legacy graph key
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "workflow"
# ── Empty actors map through registry.add() (M4) ───────────────────
Scenario: registry.add() with empty actors map does not fall back to agents map for provider/model
When I attempt to add a YAML with empty actors map and valid agents map but no top-level provider
Then a spec-yaml ValidationError should be raised containing "provider"
# ── provider_type / model_id aliases in nested config (m1) ─────────
Scenario: _extract_v2_actor extracts provider from provider_type alias in nested config
When I call _extract_v2_actor with an actors map using provider_type alias
Then the spec-yaml extracted provider should be "alias-provider"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor extracts model from model_id alias in nested config
When I call _extract_v2_actor with an actors map using model_id alias
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "alias-model"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── _extract_v2_options with empty dict (m4) ────────────────────────
Scenario: _extract_v2_options returns None for empty dict input
When I call _extract_v2_options with an empty dict
Then the spec-yaml extracted options should be None
# ── compiled_metadata value assertion (m5) ──────────────────────────
Scenario: registry.add() forwards compiled_metadata with correct values
When I add a spec-compliant YAML with schema_version "2.0" and compiled_metadata
Then the registered actor compiled metadata key "key" should have value "val"
# ── Combined actor field edge cases ────────────────────────────────
Scenario: Combined actor field without slash is ignored
When I call _extract_v2_actor with an actors map where actor field has no slash
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should contain key "actors"
And the spec-yaml extracted unsafe flag should be False
Scenario: Combined actor field does not override explicit provider but fills missing model
When I call _extract_v2_actor with an actors map where both actor and provider exist
Then the spec-yaml extracted provider should be "explicit-provider"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: Combined actor field does not override explicit model but fills missing provider
When I call _extract_v2_actor with an actors map where both actor and model exist
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "explicit-model"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Combined actor field malformed input edge cases ────────────────
Scenario: Combined actor field with empty provider part yields no provider
When I call _extract_v2_actor with an actors map where actor field has empty provider
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: Combined actor field with empty model part yields no model
When I call _extract_v2_actor with an actors map where actor field has empty model
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Combined actor field with multiple slashes (L1) ────────────────
Scenario: Combined actor field with multiple slashes splits on first slash only
When I call _extract_v2_actor with an actors map where actor field has multiple slashes
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4/extra"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── actors: null + valid agents: through registry.add() (L2) ───────
Scenario: registry.add() with actors: null falls back to valid agents: map
When I add a YAML with actors null and valid agents map
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should exist in the actor service
# ── Top-level provider_type / model_id aliases through registry.add() (T8) ──
Scenario: registry.add() accepts top-level provider_type alias
When I add a YAML with top-level provider_type alias and model
Then the actor should be registered with provider "alias-provider" and model "gpt-4"
And the registered actor should exist in the actor service
Scenario: registry.add() accepts top-level model_id alias
When I add a YAML with top-level provider and model_id alias
Then the actor should be registered with provider "openai" and model "alias-model"
And the registered actor should exist in the actor service
# ── Top-level unsafe string coercion through registry.add() (T9) ───
Scenario: registry.add() treats top-level unsafe: "yes" as not unsafe (no gate rejection)
When I add a YAML with top-level unsafe string "yes" and provider model
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
Scenario: registry.add() treats top-level unsafe: "no" as not unsafe (no gate rejection)
When I add a YAML with top-level unsafe string "no" and provider model
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
# ── Nested options extraction through registry.add() (M1) ──────────
Scenario: registry.add() extracts and preserves nested config options
When I add a spec-compliant YAML with actors map and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.9
And the registered actor config blob should contain options key "max_tokens" with value 2000
Scenario: registry.add() merges nested and top-level options (nested base, top-level overrides)
When I add a YAML with both top-level and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.7
And the registered actor config blob should contain options key "max_tokens" with value 2000
And the registered actor config blob should contain options key "top_p" with value 0.95
Scenario: registry.add() with non-dict top-level options uses nested options
When I add a YAML with non-dict top-level options and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.9
Scenario: registry.add() sets source: "yaml" default in config_blob
When I add a spec-compliant YAML with actors map and combined actor field
Then the registered actor config blob should contain source "yaml"
# ── _extract_v2_options shallow copy mutation isolation (NIT-2) ─────
Scenario: _extract_v2_options returns a shallow copy that does not mutate the original blob
When I call _extract_v2_options and mutate the returned dict
Then the original blob options should be unmodified
# ── M4: update=True creates actor when it doesn't exist ────────────
Scenario: registry.add() with update=True creates actor when it doesn't exist
@@ -518,25 +142,5 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
When upsert_actor is configured to raise RuntimeError and I add a valid YAML
Then a RuntimeError should have been propagated from add
# ── M7: actors: false (boolean) blocks agents fallback ─────────────
Scenario: registry.add() with actors: false blocks agents fallback and raises provider error
When I attempt to add a YAML with actors false and valid agents map
Then a spec-yaml ValidationError should be raised containing "provider"
# ── M8: non-dict compiled_metadata causes Pydantic error ───────────
Scenario: registry.add() with non-dict compiled_metadata raises a Pydantic validation error
When I attempt to add a valid YAML with compiled_metadata as a non-dict string
Then a Pydantic validation error should be raised for compiled_metadata
# ── M9: provider: 0 (integer zero) falls through to provider_type ──
Scenario: registry.add() with provider: 0 falls through to provider_type fallback
When I attempt to add a YAML with provider integer 0 and no provider_type
Then a spec-yaml ValidationError should be raised containing "provider"
Scenario: registry.add() with provider: 0 and valid provider_type uses provider_type
When I add a YAML with provider integer 0 and a valid provider_type
Then the actor should be registered with provider "fallback-provider" and model "gpt-4"
And the registered actor should exist in the actor service
-32
View File
@@ -26,12 +26,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
When I call ActorConfiguration.from_blob with the v3 blob
Then the configuration should have a graph_descriptor with type "graph"
Scenario: ActorConfiguration.from_blob falls through to v2 for legacy YAML
Given a v2 actor blob with agents and routes
When I call ActorConfiguration.from_blob with the v2 blob
Then the configuration should have provider "openai"
And the configuration should have model "gpt-4"
Scenario: ActorRegistry.add validates v3 LLM actor YAML
Given a mock actor service for v3 testing
And a v3 LLM actor YAML text
@@ -67,11 +61,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
And the reactive config should have a graph route
And the graph route edges should use source and target keys
Scenario: v3 format detection returns false for v2 data
Given a v2 actor config dict with agents key
When I check if the config is v3 format
Then it should not be detected as v3
# M14: test update=True path
Scenario: ActorRegistry.add with update=True overwrites existing actor
Given a mock actor service for v3 testing
@@ -174,27 +163,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
When I call ActorRegistry.add with a non-mapping YAML string
Then a ValidationError should be raised mentioning mapping
# Coverage: registry.py — _add_legacy v3 schema validation failure
Scenario: ActorRegistry._add_legacy rejects invalid v3 YAML via schema validation
Given a mock actor service for v3 testing
And a legacy v3 YAML text with invalid schema
When I call ActorRegistry.add with the legacy invalid v3 YAML
Then a ValidationError should be raised mentioning invalid v3 actor
# Coverage: registry.py — _add_legacy missing provider/model in non-v3 blob
Scenario: ActorRegistry._add_legacy rejects non-v3 blob missing provider and model
Given a mock actor service for v3 testing
And a legacy non-v3 YAML text missing provider and model
When I call ActorRegistry.add with the legacy non-v3 YAML
Then a ValidationError should be raised mentioning provider and model
# Coverage: registry.py — _add_legacy unsafe flag rejection
Scenario: ActorRegistry._add_legacy rejects unsafe legacy actor without allow_unsafe
Given a mock actor service for v3 testing
And a legacy actor YAML text marked unsafe
When I call ActorRegistry.add with the legacy unsafe YAML
Then a ValidationError should be raised mentioning unsafe
# Coverage: registry.py — upsert_actor v3 validation failure
Scenario: ActorRegistry.upsert_actor rejects invalid v3 config_blob
Given a mock actor service for v3 testing
+27 -27
View File
@@ -459,22 +459,6 @@ Feature: Consolidated Actor
Then the config options should include the overrides
Scenario: _extract_v2_actor parses v2 agents block
When I extract v2 actor from a v2-style config with agents block
Then the extracted provider and model should be set
And the graph descriptor should contain agent info
Scenario: _extract_v2_actor returns None for missing agents
When I extract v2 actor from a config without agents block
Then all extracted values should be None
Scenario: _extract_v2_options extracts options from v2 config
When I extract v2 options from a v2-style config
Then the extracted options should contain expected keys
Scenario: from_file loads and parses config from disk
Given a temporary JSON config file with provider and model
When I create an ActorConfiguration from the file
@@ -876,6 +860,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/test-actor
type: llm
description: A test actor
provider: openai
model: gpt-4
"""
@@ -888,6 +874,8 @@ Feature: Consolidated Actor
When I add actor from YAML text with schema_version "2.0":
"""
name: local/versioned
type: llm
description: A versioned actor
provider: anthropic
model: claude-3
"""
@@ -899,6 +887,8 @@ Feature: Consolidated Actor
When I add actor from YAML text with compiled_metadata:
"""
name: local/compiled
type: llm
description: A compiled actor
provider: openai
model: gpt-4o
"""
@@ -910,12 +900,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/updatable
type: llm
description: An updatable actor
provider: openai
model: gpt-4
"""
And I update actor from YAML text:
"""
name: local/updatable
type: llm
description: An updatable actor
provider: openai
model: gpt-4-turbo
"""
@@ -927,6 +921,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/removable
type: llm
description: A removable actor
provider: openai
model: gpt-4
"""
@@ -939,12 +935,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/alpha
type: llm
description: An alpha actor
provider: openai
model: gpt-4
"""
And I add actor from YAML text with update:
"""
name: local/beta
type: llm
description: A beta actor
provider: anthropic
model: claude-3
"""
@@ -957,6 +957,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/auto-prefixed
type: llm
description: An auto-prefixed actor
provider: openai
model: gpt-4
"""
@@ -968,6 +970,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/fetchable
type: llm
description: A fetchable actor
provider: openai
model: gpt-4
"""
@@ -978,6 +982,8 @@ Feature: Consolidated Actor
Given an actor registry with no configured providers for persistence
When I attempt to add actor from YAML text without name:
"""
type: llm
description: No name actor
provider: openai
model: gpt-4
"""
@@ -989,12 +995,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/duplicate
type: llm
description: A duplicate actor
provider: openai
model: gpt-4
"""
And I attempt to add duplicate actor from YAML text:
"""
name: local/duplicate
type: llm
description: A duplicate actor
provider: openai
model: gpt-4-turbo
"""
@@ -1008,17 +1018,7 @@ Feature: Consolidated Actor
And the actor "local/legacy-yaml" should have schema_version "1.5"
Scenario: Default schema version is applied when not specified
Given an actor registry with no configured providers for persistence
When I add actor from YAML text:
"""
name: local/default-version
provider: openai
model: gpt-4
"""
Then the actor "local/default-version" should have schema_version "1.0"
# ============================================================
# Originally from: actor_runtime.feature
# Feature: Tool-Calling Actor Runtime
+3 -3
View File
@@ -107,12 +107,12 @@ Feature: Plan explain and decision tree CLI commands
# plan tree - json format
# ------------------------------------------------------------------
@tdd_issue @tdd_issue_4254 @tdd_expected_fail
@tdd_issue @tdd_issue_4254
Scenario: Tree with json format
Given a set of test decisions forming a tree
When I format the tree as json
Then the json tree output should be valid json
And the json tree output should contain "decision_id"
Then the json tree output should be a valid json envelope
And the json tree output should contain "command"
# ------------------------------------------------------------------
# plan tree - yaml format
+2 -2
View File
@@ -65,13 +65,13 @@ Feature: Plan explain and tree CLI command coverage
Given pec a mock DecisionService returning a list of decisions
When pec I invoke "tree" with format "json"
Then pec the exit code should be 0
And pec the output should be valid json list
And pec the output should be valid json envelope
Scenario: Tree CLI renders yaml format
Given pec a mock DecisionService returning a list of decisions
When pec I invoke "tree" with format "yaml"
Then pec the exit code should be 0
And pec the output should contain "decision_id:"
And pec the output should contain "command:"
Scenario: Tree CLI renders table format
Given pec a mock DecisionService returning a list of decisions
@@ -17,11 +17,6 @@ from cleveragents.actor.yaml_loader import (
interpolate_env_vars,
load_yaml_text,
)
from cleveragents.actor.yaml_loader import (
_restore_template_syntax,
interpolate_env_vars,
load_yaml_text,
)
@then('an actor config ValueError should mention "{text}"')
@@ -314,76 +309,6 @@ def step_assert_options(context: Context) -> None:
assert context.actor_cfg.options["temperature"] == 0.9
@when("I extract v2 actor from a v2-style config with agents block")
def step_extract_v2_actor(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "anthropic",
"model": "claude-3",
"unsafe": True,
"options": {"max_tokens": 1000},
}
}
},
"routes": [{"from": "main_agent", "to": "end"}],
}
context.extracted = ActorConfiguration._extract_v2_actor(data)
@then("the extracted provider and model should be set")
def step_assert_extracted(context: Context) -> None:
provider, model, _graph, unsafe = context.extracted
assert provider == "anthropic"
assert model == "claude-3"
assert unsafe is True
@then("the graph descriptor should contain agent info")
def step_assert_graph_descriptor(context: Context) -> None:
_, _, graph, _ = context.extracted
assert graph is not None
assert "agent" in graph
assert "routes" in graph
@when("I extract v2 actor from a config without agents block")
def step_extract_v2_no_agents(context: Context) -> None:
context.extracted = ActorConfiguration._extract_v2_actor({"provider": "openai"})
@then("all extracted values should be None")
def step_assert_all_none(context: Context) -> None:
provider, model, graph, unsafe = context.extracted
assert provider is None
assert model is None
assert graph is None
assert unsafe is False
@when("I extract v2 options from a v2-style config")
def step_extract_v2_options(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "openai",
"model": "gpt-4",
"options": {"temperature": 0.7, "max_tokens": 500},
}
}
}
}
context.options = ActorConfiguration._extract_v2_options(data)
@then("the extracted options should contain expected keys")
def step_assert_extracted_options(context: Context) -> None:
assert context.options is not None
assert "temperature" in context.options
@when("I create an ActorConfiguration from the file")
def step_from_file(context: Context) -> None:
context.actor_cfg = ActorConfiguration.from_file(
File diff suppressed because it is too large Load Diff
@@ -525,88 +525,6 @@ def step_validation_error_mapping(context: Any) -> None:
assert "mapping" in msg, f"Error should mention 'mapping': {context.add_error}"
@given("a legacy v3 YAML text with invalid schema")
def step_legacy_v3_yaml_invalid_schema(context: Any) -> None:
# is_v3_yaml detects version starting with "3" or non-null type field.
# Provide a blob that triggers the v3 validation path but fails schema.
context.legacy_v3_yaml_invalid = yaml.safe_dump(
{
"name": "local/bad-v3",
"version": "3.0",
"type": "invalid_type_value", # not llm/graph/tool — fails ActorConfigSchema
"provider": "openai",
"model": "gpt-4",
}
)
@when("I call ActorRegistry.add with the legacy invalid v3 YAML")
def step_call_registry_add_legacy_invalid_v3(context: Any) -> None:
try:
context.registry.add(context.legacy_v3_yaml_invalid)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@then("a ValidationError should be raised mentioning invalid v3 actor")
def step_validation_error_invalid_v3(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "invalid" in msg or "v3" in msg or "validation" in msg, (
f"Error should mention invalid v3 actor: {context.add_error}"
)
@given("a legacy non-v3 YAML text missing provider and model")
def step_legacy_non_v3_yaml_missing_fields(context: Any) -> None:
context.legacy_non_v3_yaml = yaml.safe_dump(
{
"name": "local/no-provider",
# no type field → not v3, no provider/model → should fail
}
)
@when("I call ActorRegistry.add with the legacy non-v3 YAML")
def step_call_registry_add_legacy_non_v3(context: Any) -> None:
try:
context.registry.add(context.legacy_non_v3_yaml)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@then("a ValidationError should be raised mentioning provider and model")
def step_validation_error_provider_model(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "provider" in msg or "model" in msg, (
f"Error should mention 'provider' or 'model': {context.add_error}"
)
@given("a legacy actor YAML text marked unsafe")
def step_legacy_actor_yaml_unsafe(context: Any) -> None:
context.legacy_unsafe_yaml = yaml.safe_dump(
{
"name": "local/legacy-unsafe",
"provider": "openai",
"model": "gpt-4",
"unsafe": True,
}
)
@when("I call ActorRegistry.add with the legacy unsafe YAML")
def step_call_registry_add_legacy_unsafe(context: Any) -> None:
try:
context.registry.add(context.legacy_unsafe_yaml)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@when("I call ActorRegistry.upsert_actor with an invalid v3 config_blob")
def step_call_registry_upsert_invalid_v3(context: Any) -> None:
# Provide a blob that triggers is_v3_yaml() but fails ActorConfigSchema.
@@ -629,6 +547,15 @@ def step_call_registry_upsert_invalid_v3(context: Any) -> None:
context.add_error = exc
@then("a ValidationError should be raised mentioning invalid v3 actor")
def step_validation_error_invalid_v3(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "invalid" in msg or "v3" in msg or "validation" in msg, (
f"Error should mention invalid v3 actor: {context.add_error}"
)
# ── Coverage gap: config_parser.py ───────────────────────────────────────
-41
View File
@@ -93,22 +93,6 @@ def step_v3_graph_blob(context: Any) -> None:
}
@given("a v2 actor blob with agents and routes")
def step_v2_blob(context: Any) -> None:
context.v2_blob = {
"agents": {
"main": {
"type": "llm",
"config": {
"provider": "openai",
"model": "gpt-4",
},
}
},
"routes": {},
}
@given("a mock actor service for v3 testing")
def step_mock_actor_service(context: Any) -> None:
mock_actor_service = MagicMock()
@@ -247,13 +231,6 @@ def step_v3_graph_config_dict(context: Any) -> None:
}
@given("a v2 actor config dict with agents key")
def step_v2_config_dict(context: Any) -> None:
context.v2_config = {
"agents": {"main": {"type": "llm", "config": {"provider": "openai"}}},
}
@given("an existing actor in the mock service")
def step_existing_actor(context: Any) -> None:
"""Set up mock to return an existing actor for duplicate checks."""
@@ -274,14 +251,6 @@ def step_call_from_blob_v3(context: Any) -> None:
)
@when("I call ActorConfiguration.from_blob with the v2 blob")
def step_call_from_blob_v2(context: Any) -> None:
context.actor_config = ActorConfiguration.from_blob(
blob=context.v2_blob,
name="local/test",
)
@when("I call ActorRegistry.add with the v3 YAML")
def step_call_registry_add_v3(context: Any) -> None:
context.result_actor = context.registry.add(context.v3_yaml_text)
@@ -329,11 +298,6 @@ def step_parse_v3_config(context: Any) -> None:
context.reactive_config = parser._build(context.v3_config)
@when("I check if the config is v3 format")
def step_check_v3_format(context: Any) -> None:
context.is_v3 = ReactiveConfigParser._is_v3_format(context.v2_config)
# ── Then steps ───────────────────────────────────────────────────────────
@@ -470,11 +434,6 @@ def step_reactive_has_graph_route(context: Any) -> None:
assert len(route.edges) >= 1, f"Expected at least 1 edge, got {len(route.edges)}"
@then("it should not be detected as v3")
def step_not_v3(context: Any) -> None:
assert context.is_v3 is False, "Expected v2 data to NOT be detected as v3"
@then("the graph route edges should use source and target keys")
def step_graph_edges_use_source_target(context: Any) -> None:
routes = context.reactive_config.routes
@@ -807,6 +807,22 @@ def step_pec_output_valid_json_list(context: Context) -> None:
assert isinstance(data, list), "Expected JSON array"
@then("pec the output should be valid json envelope")
def step_pec_output_valid_json_envelope(context: Context) -> None:
parsed = json.loads(context.pec_result.output.strip())
assert isinstance(parsed, dict), f"Expected JSON object, got {type(parsed)}"
assert _ENVELOPE_KEYS.issubset(parsed.keys()), (
f"Expected envelope keys {_ENVELOPE_KEYS}, got {set(parsed.keys())}"
)
data = parsed["data"]
assert isinstance(data, dict), (
f"Expected envelope data to be a dict, got {type(data)}"
)
assert "plan_id" in data, (
f"Expected 'plan_id' in envelope data, got {set(data.keys())}"
)
@then("pec the tree should exclude the superseded grandchild")
def step_pec_tree_excludes_orphan(context: Context) -> None:
# Tree should have exactly one root with one child and zero grandchildren.
+12
View File
@@ -405,6 +405,18 @@ def step_tree_json_valid(context: Context) -> None:
assert isinstance(parsed, list), "Expected a JSON array"
@then("the json tree output should be a valid json envelope")
def step_tree_json_valid_envelope(context: Context) -> None:
parsed = json.loads(context.pe_tree_json)
assert isinstance(parsed, dict), (
f"Expected a JSON object (envelope), got {type(parsed)}"
)
_envelope_keys = {"command", "status", "exit_code", "data", "timing", "messages"}
assert _envelope_keys.issubset(parsed.keys()), (
f"Expected envelope keys {_envelope_keys}, got {set(parsed.keys())}"
)
@then('the json tree output should contain "{text}"')
def step_tree_json_contains(context: Context, text: str) -> None:
assert text in context.pe_tree_json, f"Expected '{text}' in tree JSON"
-34
View File
@@ -4,40 +4,6 @@ Library Collections
Library Process
*** Test Cases ***
V2 Actor Config Produces Provider And Graph Descriptor
${config}= Set Variable ${OUTPUT DIR}/v2_actor.yaml
${content}= Catenate SEPARATOR=\n
... cleveragents:
... ${SPACE*2}default_router: main_router
... agents:
... ${SPACE*2}paper_writer:
... ${SPACE*4}type: llm
... ${SPACE*4}config:
... ${SPACE*6}provider: openai
... ${SPACE*6}model: gpt-4
... ${SPACE*6}unsafe: true
... ${SPACE*6}options:
... ${SPACE*8}temperature: 0.5
... routes:
... ${SPACE*2}main_router:
... ${SPACE*4}type: stream
... ${SPACE*4}operators:
... ${SPACE*6}- type: map
... ${SPACE*8}params:
... ${SPACE*10}agent: paper_writer
... ${SPACE*4}publications:
... ${SPACE*6}- __output__
Create File ${config} ${content}
${result}= Run Process ${PYTHON} robot/helper_actor_config.py ${config}
Should Be Equal As Integers ${result.rc} 0
${payload}= Evaluate __import__('json').loads('''${result.stdout.strip()}''')
Should Be Equal ${payload['provider']} openai
Should Be Equal ${payload['model']} gpt-4
Should Be True ${payload['unsafe']}
List Should Contain Value ${payload['graph_keys']} agents
List Should Contain Value ${payload['graph_keys']} routes
Should Be Equal As Numbers ${payload['options']['temperature']} 0.5
Missing Config File Exits With Non-Zero Code And Stderr Message
[Tags] tdd_issue tdd_issue_4202 tdd_expected_fail
${missing}= Set Variable ${OUTPUT DIR}/does_not_exist_actor.yaml
+18 -5
View File
@@ -386,11 +386,24 @@ M6 E2E Hierarchical Decomposition Via Plan Tree
# P0-4: Hard assertion — tree command must succeed.
Should Be Equal As Integers ${tree.rc} 0 msg=plan tree failed (rc=${tree.rc}): ${tree.stderr}
Should Not Be Empty ${tree.stdout} Plan tree output should not be empty
# P0-4: Hard assertion — at least one decision node must exist after execution.
${decision_count}= Evaluate $tree.stdout.count('"decision_id"')
Log Decision tree contains ${decision_count} decision node(s)
Should Be True ${decision_count} >= 1
... Plan tree should contain at least one decision node after execution (found ${decision_count})
# P0-4: Hard assertion — tree output must contain the spec-required envelope.
# The new envelope format wraps the tree in a command envelope with
# "command", "status", "exit_code", "data", "timing", "messages" keys.
# The "data" field contains "plan_id", "tree", "summary", "child_plans",
# and "decision_ids" (a mapping of human-readable keys to decision ULIDs).
${has_command_key}= Evaluate '"command"' in $tree.stdout
Should Be True ${has_command_key}
... Plan tree JSON output should contain spec-required envelope "command" key
${has_data_key}= Evaluate '"data"' in $tree.stdout
Should Be True ${has_data_key}
... Plan tree JSON output should contain spec-required envelope "data" key
${has_plan_id_key}= Evaluate '"plan_id"' in $tree.stdout
Should Be True ${has_plan_id_key}
... Plan tree JSON output should contain "plan_id" in envelope data
# Check for decision_ids mapping (proves at least one decision was recorded)
${has_decision_ids}= Evaluate '"decision_ids"' in $tree.stdout
Should Be True ${has_decision_ids}
... Plan tree should contain decision_ids mapping after execution
# Check for hierarchical children (decomposition infrastructure indicator)
${has_children_key}= Evaluate '"children"' in $tree.stdout
IF ${has_children_key}
+70 -194
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Literal, cast
from typing import Any, cast
from pydantic import BaseModel, Field
@@ -42,7 +42,7 @@ class ActorConfiguration(BaseModel):
@staticmethod
def load_blob_from_file(config_path: Path) -> dict[str, Any]:
"""Load a configuration blob from a JSON or YAML file (v2 parity)."""
"""Load a configuration blob from a JSON or YAML file."""
path = config_path.expanduser()
if not path.exists():
raise ValueError(f"Config file not found: {path}")
@@ -88,36 +88,25 @@ class ActorConfiguration(BaseModel):
) -> ActorConfiguration:
"""Coerce an incoming config blob plus CLI overrides into a validated model.
Supports both the legacy v2 ``agents/routes`` YAML layout and the v3
``ActorConfigSchema`` format (identified by a top-level ``type`` key
whose value is one of ``llm``, ``graph``, or ``tool``).
Supports the v3 ``ActorConfigSchema`` format (identified by a top-level
``type`` key whose value is one of ``llm``, ``graph``, or ``tool``).
Provider and model may be at the top level OR nested inside
``actors.<actor-name>.config``.
"""
data: dict[str, Any] = dict(blob) if isinstance(blob, dict) else {}
# Try v3 extraction first (presence of 'type' with a v3 value).
v3_provider, v3_model, v3_graph, v3_unsafe = cls._extract_v3_actor(data)
# Fall back to v2 extraction when v3 didn't match.
v2_provider, v2_model, v2_graph, v2_unsafe = cls._extract_v2_actor(data)
v2_options = cls._extract_v2_options(data)
resolved_provider = (
provider
or data.get("provider")
or data.get("provider_type")
or v3_provider
or v2_provider
)
resolved_model = (
model or data.get("model") or data.get("model_id") or v3_model or v2_model
provider or data.get("provider") or data.get("provider_type") or v3_provider
)
resolved_model = model or data.get("model") or data.get("model_id") or v3_model
resolved_graph = (
graph_descriptor
or data.get("graph_descriptor")
or data.get("graph")
or v3_graph
or v2_graph
)
options_raw = data.get("options")
@@ -127,8 +116,6 @@ class ActorConfiguration(BaseModel):
merged_options: dict[str, Any] = {}
if default_options:
merged_options.update(default_options)
if v2_options:
merged_options.update(v2_options)
if options_value:
merged_options.update(options_value)
if option_overrides:
@@ -136,10 +123,7 @@ class ActorConfiguration(BaseModel):
top_unsafe_raw = data.get("unsafe", False)
resolved_unsafe = (
(top_unsafe_raw is True or top_unsafe_raw == 1)
or v3_unsafe
or v2_unsafe
or unsafe
(top_unsafe_raw is True or top_unsafe_raw == 1) or v3_unsafe or unsafe
)
if not resolved_provider:
@@ -167,27 +151,6 @@ class ActorConfiguration(BaseModel):
unsafe=resolved_unsafe,
)
@staticmethod
@staticmethod
def _resolve_actors_map(
data: dict[str, Any],
) -> tuple[Any, Literal["actors", "agents"]]:
"""Resolve the actors/agents map from a parsed YAML blob.
The spec allows either ``actors:`` or ``agents:`` as the top-level
map key (oneOf). ``actors:`` is preferred (spec-canonical); the
``agents:`` key is only consulted when ``actors:`` is absent or
explicitly ``null``.
Returns ``(map_obj, map_key)`` where *map_obj* is the raw value
(may be ``None``, a non-dict, or a dict) and *map_key* is the
string ``"actors"`` or ``"agents"``.
"""
actors_val = data.get("actors")
if actors_val is not None:
return actors_val, "actors"
return data.get("agents"), "agents"
@staticmethod
def _extract_v3_actor(
data: dict[str, Any],
@@ -195,13 +158,13 @@ class ActorConfiguration(BaseModel):
"""Derive provider/model/graph from the v3 Actor YAML schema format.
The v3 schema is identified by a top-level ``type`` key whose value
is one of ``llm``, ``graph``, or ``tool``. The model is taken from
the top-level ``model`` key. Because v3 YAML does not carry a
``provider`` field the provider is inferred from the model string:
is one of ``llm``, ``graph``, or ``tool`` OR by a top-level
``actors:`` map containing at least one entry whose ``config:`` block
carries a ``type`` field with one of those values.
* If the model contains ``/`` the prefix is used as provider
(e.g. ``openai/gpt-4`` provider ``openai``).
* Otherwise ``"custom"`` is used as a fallback.
Provider and model may appear at the top level OR nested inside
``actors.<actor-name>.config``. When found in the nested config,
they take precedence over top-level values.
For ``type: graph`` actors the ``route`` block is wrapped into a
graph descriptor.
@@ -211,28 +174,65 @@ class ActorConfiguration(BaseModel):
may be ``None`` if the data does not look like v3.
"""
actor_type = data.get("type")
if not isinstance(actor_type, str):
return None, None, None, False
if actor_type.lower() not in _V3_ACTOR_TYPES:
return None, None, None, False
if not isinstance(actor_type, str) or actor_type.lower() not in _V3_ACTOR_TYPES:
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
nested_type = config_block.get("type")
is_v3_type = (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
)
if is_v3_type:
actor_type = nested_type
break
break
if not isinstance(actor_type, str):
return None, None, None, False
model_value = data.get("model")
# C2: model is optional for TOOL actors — only reject when model
# is absent for types that require it (LLM, GRAPH).
if not model_value or not isinstance(model_value, str):
if actor_type.lower() != "tool":
return None, None, None, False
# TOOL actors may omit model; use empty string as sentinel.
model_value = ""
# Infer provider from model string or default to "custom".
provider_value = (
infer_provider_from_model(model_value) if model_value else "custom"
)
provider_value = data.get("provider")
unsafe_flag = bool(data.get("unsafe", False))
# Build a graph descriptor for graph actors from the route block.
if not model_value or not isinstance(model_value, str):
if actor_type.lower() != "tool":
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
mv = config_block.get("model")
if isinstance(mv, str) and mv:
model_value = mv
break
break
if not model_value:
if actor_type.lower() != "tool":
return None, None, None, False
model_value = ""
if not provider_value or not isinstance(provider_value, str):
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
provider_value = pv
break
break
if not provider_value:
provider_value = (
infer_provider_from_model(model_value) if model_value else "custom"
)
graph_descriptor: dict[str, Any] | None = None
if actor_type.lower() == "graph":
route_raw = data.get("route")
@@ -244,127 +244,3 @@ class ActorConfiguration(BaseModel):
}
return provider_value, model_value, graph_descriptor, unsafe_flag
@staticmethod
def _extract_v2_actor(
data: dict[str, Any],
) -> tuple[str | None, str | None, dict[str, Any] | None, bool]:
"""Derive provider/model/graph from the v2/spec YAML actor config format.
Supports both the legacy ``agents:`` map key and the spec-compliant
``actors:`` map key. Within each actor's ``config:`` block, the
combined ``actor`` field (e.g. ``"openai/gpt-4"``) is also accepted
as an alternative to separate ``provider`` + ``model`` fields.
"""
map_obj, map_key = ActorConfiguration._resolve_actors_map(data)
# NOTE: Any non-None value for ``actors:`` — including falsy values
# such as ``actors: {}``, ``actors: false``, ``actors: 0``, or
# ``actors: ""`` — blocks the ``agents:`` fallback. This is
# intentional: a present ``actors:`` key explicitly declares the
# actors map (even if empty or invalid), whereas ``actors: null``
# (or absent) defers to ``agents:``.
#
# NOTE: Only the **first** entry in the actors/agents map is inspected
# for provider, model, unsafe, and graph descriptor. The ``add()``
# method rejects multi-actor YAML (>1 entry) with a ``ValidationError``
# to prevent silent data loss. The multi-actor case is handled by the
# v3 schema validation path.
if isinstance(map_obj, dict) and map_obj:
map_dict = cast(dict[str, Any], map_obj)
# Only the first actor entry is used for provider/model extraction.
for actor_key, first_entry in map_dict.items():
if isinstance(first_entry, dict):
entry_dict = cast(dict[str, Any], first_entry)
config_block = entry_dict.get("config")
if isinstance(config_block, dict):
config_dict = cast(dict[str, Any], config_block)
# Support the combined ``actor`` field format
# (e.g. ``"openai/gpt-4"`` → provider + model).
# NOTE: ``provider_type`` and ``model_id`` are
# accepted as aliases for backward compatibility,
# even though the spec's JSON schema for the
# ``config:`` block only lists ``provider``,
# ``model``, and ``actor``.
# Use ``is not None`` (not ``or``) so that falsy
# values like ``""`` or ``0`` are preserved rather
# than falling through to the alias.
provider_value = config_dict.get("provider")
if provider_value is None:
provider_value = config_dict.get("provider_type")
model_value = config_dict.get("model")
if model_value is None:
model_value = config_dict.get("model_id")
combined_actor = config_dict.get("actor")
if (
combined_actor
and isinstance(combined_actor, str)
and "/" in combined_actor
):
parts = combined_actor.split("/", 1)
if not provider_value:
provider_value = parts[0]
if not model_value:
model_value = parts[1]
unsafe_raw = config_dict.get("unsafe", False)
# Accept only boolean ``True`` or integer ``1`` as
# unsafe. Note: ``unsafe_raw == 1`` also matches
# ``1.0`` (float) due to Python's numeric equality
# rules — this is fail-safe (treats 1.0 as unsafe).
unsafe_flag = unsafe_raw is True or unsafe_raw == 1
# The graph descriptor is always built and returned
# when a valid ``config:`` block is found, regardless
# of whether ``provider``/``model`` were extracted.
# This allows the caller to preserve the graph
# structure even when provider/model come from
# elsewhere (e.g. top-level fields).
descriptor: dict[str, Any] = {
"agent": actor_key,
map_key: dict(map_dict),
}
for key in (
"routes",
"merges",
"templates",
"cleveragents",
):
if key in data and data[key] is not None:
descriptor[key] = data[key]
return (
str(provider_value) if provider_value else None,
str(model_value) if model_value else None,
descriptor,
unsafe_flag,
)
break # first entry only
return None, None, None, False
@staticmethod
def _extract_v2_options(data: dict[str, Any]) -> dict[str, Any] | None:
"""Extract option defaults from the v2/spec actor config format.
Supports both ``actors:`` (spec-canonical) and ``agents:`` (legacy)
map keys.
"""
map_obj, _ = ActorConfiguration._resolve_actors_map(data)
if isinstance(map_obj, dict) and map_obj:
map_dict = cast(dict[str, Any], map_obj)
for _, first_entry in map_dict.items():
if isinstance(first_entry, dict):
entry_dict = cast(dict[str, Any], first_entry)
config_block = entry_dict.get("config")
if isinstance(config_block, dict):
config_dict = cast(dict[str, Any], config_block)
options = config_dict.get("options")
if isinstance(options, dict):
# Return a shallow copy so downstream mutations
# (e.g. ``config_blob["options"]`` updates) do
# not propagate back into the original blob.
return dict(cast(dict[str, Any], options))
break # first entry only
return None
-171
View File
@@ -1,171 +0,0 @@
"""Legacy (v2) actor registration logic extracted from ``ActorRegistry``.
This module handles the v2 blob registration path, including provider/model
extraction, unsafe flag enforcement, and nested option merging. It is used
by :class:`ActorRegistry` to keep the main registry module under the 500-line
limit.
"""
from __future__ import annotations
from typing import Any
import pydantic
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.schema import ActorConfigSchema, is_v3_yaml
from cleveragents.application.services.actor_service import ActorService
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core import Actor
def add_legacy(
blob: dict[str, Any],
*,
yaml_text: str,
update: bool,
schema_version: str,
compiled_metadata: dict[str, Any] | None,
unsafe: bool = False,
allow_unsafe: bool = False,
actor_service: ActorService,
ensure_namespaced: callable, # type: ignore[type-arg]
) -> Actor:
"""Register an actor from a legacy (v2) blob.
Args:
blob: Parsed YAML blob.
yaml_text: Original YAML source text.
update: When ``True`` allow overwriting an existing actor.
schema_version: Schema version string.
compiled_metadata: Optional compiler-produced metadata dict.
unsafe: Asserts that the actor IS unsafe.
allow_unsafe: Permits registration of an already-unsafe actor.
actor_service: The actor persistence service.
ensure_namespaced: Function to namespace actor names.
Returns:
The persisted ``Actor`` domain object.
"""
name_raw: str = blob.get("name", "")
if not name_raw:
raise ValidationError("Actor YAML must include a 'name' field.")
# ── Validate v3 YAML via ActorConfigSchema if detected ──────────────────
# This ensures v3 actors are validated against the full schema, including
# cycle detection, required fields, and enum validation.
blob_is_v3 = is_v3_yaml(blob)
if blob_is_v3:
try:
ActorConfigSchema.model_validate(blob)
except pydantic.ValidationError as exc:
raise ValidationError(f"Invalid v3 actor configuration: {exc}") from exc
name = ensure_namespaced(name_raw)
provider_raw = blob.get("provider") or blob.get("provider_type", "")
model_raw = blob.get("model") or blob.get("model_id", "")
# ── Guard: reject multi-actor YAML ─────────────────────────────────
# ``add()`` is designed for single-actor YAML files. Provider, model,
# unsafe, and graph extraction all operate on the *first* entry only,
# so a multi-actor map would silently discard later entries (including
# their ``unsafe`` flags — a spec-compliance and security concern).
# Multi-actor configurations must be split and registered individually.
# Uses ``_resolve_actors_map()`` for consistent ``actors:``/``agents:``
# resolution across all call sites.
actors_map, _ = ActorConfiguration._resolve_actors_map(blob)
if isinstance(actors_map, dict) and len(actors_map) > 1:
raise ValidationError(
"registry.add() only supports single-actor YAML files. "
"Multi-actor configurations must be registered individually."
)
# Always extract from the nested ``actors:``/``agents:`` map so that
# ``unsafe`` and ``graph_descriptor`` are captured even when top-level
# ``provider``/``model`` are present. This matches the behaviour of
# ``from_blob()`` which unconditionally calls ``_extract_v2_actor()``.
nested_provider, nested_model, nested_graph, nested_unsafe = (
ActorConfiguration._extract_v2_actor(blob)
)
if not provider_raw and nested_provider:
provider_raw = nested_provider
if not model_raw and nested_model:
model_raw = nested_model
if not blob_is_v3 and (not provider_raw or not model_raw):
raise ValidationError("Actor YAML must include 'provider' and 'model' fields.")
# For v3 actors without provider/model (e.g. TOOL actors), provider_raw
# and model_raw may be empty strings here. This is expected — the legacy
# canonicalization path in upsert_actor() / from_blob() will reject
# provider-less TOOL actors. See follow-up issue #9971.
provider = str(provider_raw) if provider_raw else ""
model = str(model_raw) if model_raw else ""
top_unsafe_raw = blob.get("unsafe", False)
# Accept only boolean ``True`` or integer ``1`` as unsafe. Note:
# ``top_unsafe_raw == 1`` also matches ``1.0`` (float) due to
# Python's numeric equality rules — this is fail-safe (treats 1.0
# as unsafe).
#
# ``unsafe`` (the parameter) is an *assertion* — "this actor IS
# unsafe" (maps to the spec's "CLI --unsafe flag").
# ``allow_unsafe`` is a *permission* — "I PERMIT an already-unsafe
# actor to be registered" but does NOT itself make the actor unsafe.
# Therefore only ``unsafe`` participates in the stored value; the
# spec rule is: "the result is true if *any* of: top-level unsafe,
# nested unsafe, or the CLI --unsafe flag is true."
effective_unsafe = (
(top_unsafe_raw is True or top_unsafe_raw == 1) or nested_unsafe or unsafe
)
if effective_unsafe and not (unsafe or allow_unsafe):
raise ValidationError(
"Actor configuration is marked unsafe; pass unsafe=True "
"or allow_unsafe=True to confirm."
)
if not update:
try:
actor_service.get_actor(name)
except NotFoundError:
pass # Actor does not exist yet — proceed with add
else:
raise ValidationError(
f"Actor '{name}' already exists. Pass update=True to overwrite."
)
# ── Extract nested options so they are not silently discarded ─────
# ``from_blob()`` calls ``_extract_v2_options()`` internally, but
# ``add()`` bypasses ``from_blob()``. Without this call, options
# nested inside ``actors.<name>.config.options`` would be lost.
v2_options = ActorConfiguration._extract_v2_options(blob)
config_blob: dict[str, Any] = dict(blob)
config_blob.setdefault("source", "yaml")
if v2_options is not None:
existing_options = config_blob.get("options")
if existing_options is None or not isinstance(existing_options, dict):
# No valid top-level options dict — use nested options directly.
config_blob["options"] = v2_options
else:
# Both top-level and nested options exist. Match ``from_blob()``
# merge semantics: nested options as base, top-level overrides.
merged = dict(v2_options)
merged.update(existing_options)
config_blob["options"] = merged
top_graph_raw = blob.get("graph_descriptor")
top_graph = top_graph_raw if top_graph_raw is not None else blob.get("graph")
resolved_graph = top_graph if top_graph is not None else nested_graph
return actor_service.upsert_actor(
name=name,
provider=provider,
model=model,
config_blob=config_blob,
graph_descriptor=resolved_graph,
unsafe=effective_unsafe,
set_default=False,
yaml_text=yaml_text,
schema_version=schema_version,
compiled_metadata=compiled_metadata,
)
+5 -15
View File
@@ -26,7 +26,6 @@ import pydantic
import yaml
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.legacy_registry import add_legacy
from cleveragents.actor.schema import ActorConfigSchema, is_v3_yaml
from cleveragents.actor.v3_registry import add_v3, is_v3_blob
from cleveragents.application.services.actor_service import ActorService
@@ -326,6 +325,7 @@ class ActorRegistry:
# v3 path: validate against ActorConfigSchema when detected
# ----------------------------------------------------------
if is_v3_blob(blob):
effective_allow_unsafe = allow_unsafe or unsafe
return add_v3(
blob,
yaml_text=yaml_text,
@@ -333,22 +333,12 @@ class ActorRegistry:
schema_version=version,
compiled_metadata=compiled_metadata,
actor_service=self._actor_service,
allow_unsafe=allow_unsafe,
allow_unsafe=effective_allow_unsafe,
)
# ----------------------------------------------------------
# Legacy (v2) path: flat provider/model extraction
# ----------------------------------------------------------
return add_legacy(
blob,
yaml_text=yaml_text,
update=update,
schema_version=version,
compiled_metadata=compiled_metadata,
unsafe=unsafe,
allow_unsafe=allow_unsafe,
actor_service=self._actor_service,
ensure_namespaced=self._ensure_namespaced,
raise ValidationError(
"Invalid actor configuration: no valid type field found. "
"Expected a v3 YAML with type: llm, type: graph, or type: tool."
)
# ------------------------------------------------------------------
+108 -2
View File
@@ -26,9 +26,111 @@ logger = logging.getLogger(__name__)
def is_v3_blob(blob: dict[str, Any]) -> bool:
"""Return ``True`` when *blob* looks like a v3 ``ActorConfigSchema``."""
"""Return ``True`` when *blob* looks like a v3 ``ActorConfigSchema``.
A blob is considered v3 if it has:
1. A top-level ``type`` field with a v3 value (llm/graph/tool), OR
2. An ``actors:`` map containing at least one entry whose ``type`` field
has a v3 value (nested actor format per spec line 21417).
The nested format allows configurations like:
name: local/my-actor
actors:
my_actor:
type: llm
config:
provider: anthropic
model: claude-3.5-sonnet
Returns:
True if the blob appears to be v3 format.
"""
actor_type = blob.get("type")
return isinstance(actor_type, str) and actor_type.lower() in _V3_ACTOR_TYPES
if isinstance(actor_type, str) and actor_type.lower() in _V3_ACTOR_TYPES:
return True
actors_val = blob.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, entry in actors_val.items():
if isinstance(entry, dict):
nested_type = entry.get("type")
if (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
):
return True
return False
def _extract_nested_v3_config(data: dict[str, Any]) -> None:
"""Extract provider/model/type/description/name from nested actors.<name> block.
Per spec line 21417, v3 YAML may have provider/model at the top level
OR nested inside ``actors.<actor-name>.config``. When found in the nested
config, values are promoted to the top level.
Similarly, ``type``, ``description``, and ``name`` may be at the top level
OR nested inside ``actors.<actor-name>`` (for type/description) or
``actors.<actor-name>.config`` (for name). This function promotes all
of these from their nested locations when not present at the top level.
Args:
data: The actor blob (modified in place).
"""
actors_val = data.get("actors")
if not isinstance(actors_val, dict) or not actors_val:
return
for _, entry in actors_val.items():
if not isinstance(entry, dict):
continue
if "type" not in data:
type_val = entry.get("type")
if isinstance(type_val, str) and type_val:
data["type"] = type_val
if "description" not in data:
desc_val = entry.get("description")
if isinstance(desc_val, str) and desc_val:
data["description"] = desc_val
if "name" not in data:
name_val = entry.get("name")
if isinstance(name_val, str) and name_val:
data["name"] = name_val
config_block = entry.get("config")
if isinstance(config_block, dict):
if "model" not in data:
mv = config_block.get("model")
if isinstance(mv, str) and mv:
data["model"] = mv
if "provider" not in data:
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
data["provider"] = pv
if "name" not in data:
name_val = config_block.get("name")
if isinstance(name_val, str) and name_val:
data["name"] = name_val
if "description" not in data:
desc_val = config_block.get("description")
if isinstance(desc_val, str) and desc_val:
data["description"] = desc_val
if (
"type" in data
and "description" in data
and "model" in data
and "provider" in data
):
break
def add_v3(
@@ -73,6 +175,10 @@ def add_v3(
if isinstance(name_raw, str) and name_raw and "/" not in name_raw:
data["name"] = f"local/{name_raw}"
# Extract provider and model from nested actors.<name>.config block if present.
# This handles the v3 nested format per spec line 21417.
_extract_nested_v3_config(data)
# Infer provider before schema validation — the schema now requires
# provider for LLM and GRAPH actors (added by #5869).
if not data.get("provider"):
+138 -2
View File
@@ -27,7 +27,7 @@ import re
import shutil
import time
from contextlib import suppress
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
@@ -4117,6 +4117,131 @@ def _get_decision_label(decision_type: str, per_type_ordinal: int = 0) -> str:
return base_label
def _build_tree_data(
plan_id: str,
tree_data: list[dict[str, object]],
decisions: list[Decision],
show_superseded: bool = False,
started_at: datetime | None = None,
) -> dict[str, object]:
"""Build the data payload for ``agents plan tree --format json/yaml``.
Returns the ``data`` dict that will be wrapped in the spec-required
command envelope by ``format_output``.
"""
filtered = (
decisions if show_superseded else [d for d in decisions if not d.is_superseded]
)
def count_nodes(nodes: list[dict[str, object]]) -> int:
count = 0
for node in nodes:
count += 1
children = node.get("children", [])
if isinstance(children, list):
count += count_nodes(children)
return count
def compute_depth(nodes: list[dict[str, object]]) -> int:
if not nodes:
return 0
max_depth = 0
for node in nodes:
children = node.get("children", [])
if isinstance(children, list) and children:
max_depth = max(max_depth, 1 + compute_depth(children))
return max_depth
nodes_count = count_nodes(tree_data)
tree_depth = compute_depth(tree_data)
child_plan_ids: set[str] = set()
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn") and d.plan_id:
child_plan_ids.add(d.plan_id)
child_plans_count = len(child_plan_ids)
child_plans_str = f"{child_plans_count}+" if child_plans_count > 0 else "0"
invariants_count = sum(
1 for d in filtered if d.decision_type == "invariant_enforced"
)
superseded_count = sum(1 for d in decisions if d.is_superseded)
summary = {
"nodes": nodes_count,
"depth": tree_depth,
"child_plans": child_plans_str,
"invariants": invariants_count,
"superseded": superseded_count,
}
type_counts: dict[str, int] = {}
decision_ids: dict[str, str] = {}
for d in filtered:
type_counts[d.decision_type] = type_counts.get(d.decision_type, 0) + 1
ordinal = type_counts[d.decision_type]
if d.decision_type == "prompt_definition":
key = "root"
elif d.decision_type == "invariant_enforced":
key = f"invariant_{ordinal}"
elif d.decision_type == "strategy_choice":
key = "strategy"
elif d.decision_type == "implementation_choice":
key = f"implementation_{ordinal}"
elif d.decision_type == "subplan_spawn":
key = f"spawn_{ordinal}"
elif d.decision_type == "subplan_parallel_spawn":
key = f"parallel_{ordinal}"
else:
key = f"{d.decision_type}_{ordinal}"
decision_ids[key] = d.decision_id
child_plans_list: list[dict[str, object]] = []
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn") and d.plan_id:
child_plans_list.append(
{
"id": d.plan_id,
"phase": "execute",
"state": "queued",
}
)
def convert_tree_node(node: dict[str, object]) -> dict[str, object]:
"""Convert internal tree node format to spec format."""
spec_node: dict[str, object] = {
"type": node.get("type"),
"description": node.get("question") or node.get("description"),
}
if node.get("confidence") is not None:
spec_node["confidence"] = node.get("confidence")
if node.get("type") in ("subplan_spawn", "subplan_parallel_spawn"):
spec_node["plan_id"] = node.get("plan_id", "")
children = node.get("children", [])
if isinstance(children, list) and children:
spec_node["children"] = [convert_tree_node(child) for child in children]
return spec_node
spec_tree = convert_tree_node(tree_data[0]) if tree_data else None
return {
"plan_id": plan_id,
"tree": spec_tree,
"summary": summary,
"child_plans": child_plans_list,
"decision_ids": decision_ids,
}
@app.command("tree")
def tree_decisions_cmd(
plan_id: Annotated[
@@ -4139,6 +4264,7 @@ def tree_decisions_cmd(
"""Display the decision tree for a plan."""
from cleveragents.application.container import get_container
_tree_cmd_start = datetime.now(UTC)
container = get_container()
svc = container.decision_service()
decisions = svc.list_decisions(plan_id)
@@ -4153,7 +4279,17 @@ def tree_decisions_cmd(
)
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
console.print(format_output(tree_data, fmt))
tree_data_dict = _build_tree_data(
plan_id, tree_data, decisions, show_superseded, started_at=_tree_cmd_start
)
console.print(
format_output(
tree_data_dict,
fmt,
command="plan tree",
messages=[{"level": "ok", "text": "Decision tree rendered"}],
)
)
elif fmt == OutputFormat.TABLE:
# Flatten for table view
filtered = (