Deprecate and remove BaseAction; partition the action layer by action type (single-entity / bulk / scope)
- Dominant language
- Python
- Stars
- 670
- Forks
- 183
- Avg merge
- 15h 13m
- Merged PRs (30d)
- 368
Description
Summary
=======
Remove the catch-all BaseAction base class. Make the three action-type bases - BaseSingleEntityAction, BaseBulkAction, BaseScopeAction - fully independent (each carrying its own common contract), and partition the action execution framework (Processor / Runner / Validator / Monitor) per action type rather than having one generic framework keyed on BaseAction.
Background & Motivation
=======================
The action layer currently funnels every action through a single ABC, BaseAction (src/ai/backend/manager/actions/action/base.py), which exposes a lowest-common-denominator contract:
- entity_id() -> str | None (optional, defaults to None)
- entity_type() -> EntityType (abstract classmethod)
- operation_type() -> ActionOperationType (abstract classmethod)
- spec() -> ActionSpec
Type-specific bases already exist but still inherit BaseAction, so the richer per-type contracts are bolted on top of the generic one:
- BaseSingleEntityAction adds target_entity_id(), target_element(), field_data()
- BaseBulkAction[TTarget] adds targets() (individual processing, partial failures allowed)
- BaseScopeAction adds scope_type(), scope_id(), target_element()
Problems with the current shape:
1. Weak typing at the framework boundary. ActionProcessor[TAction: BaseAction, ...], ActionValidator.validate(action: BaseAction, ...), and the ActionMonitor hooks only see BaseAction. RBAC validation differs fundamentally by type (single entity -> target_element(); scope -> scope_type()/scope_id(); bulk -> element_refs()), but the generic framework cannot express this - validators must downcast.
2. BaseAction is mid-migration debris. ~46 entity bases (UserAction(BaseAction), AgentAction(BaseAction), ImageAction(BaseAction), ...) still inherit BaseAction directly, with concrete actions defining operation_type() ad hoc and no type semantics. The already-migrated entities (keypair, group, project, session, user-RBAC, vfolder, model_deployment) demonstrate the target pattern.
Goal
====
- BaseAction is completely removed from the codebase.
- Each action type has its own self-contained base action AND its own framework path (processor/runner/validator/monitor) typed to that base.
- No single shared supertype remains across the three types; search actions are expressed via the bulk or scope base (no dedicated BaseSearchAction).
Proposed Design
===============
1. Self-contained type bases
BaseSingleEntityAction, BaseBulkAction, BaseScopeAction each define the previously-shared contract themselves (entity_id, entity_type, operation_type, spec) and no longer inherit BaseAction. Minor duplication of spec()/entity_id() across the three files is accepted as the cost of independence. Mirror this on the result side (BaseSingleEntityActionResult, BaseBulkActionResult, BaseScopeActionResult).
2. Per-type framework
Replace the single generic framework with type-specific implementations, each bound to its own base:
- SingleEntityActionProcessor / Runner / Validator / Monitor - bound to BaseSingleEntityAction(Result)
- BulkActionProcessor / ... - bound to BaseBulkAction(Result)
- ScopeActionProcessor / ... - bound to BaseScopeAction(Result)
Each path can then use its type's full contract directly (e.g. the single-entity validator reads target_element(); the scope validator reads scope_type()/scope_id(); the bulk monitor iterates element_refs()).
3. Remove BaseAction
After all entities migrate, delete BaseAction, the TAction/TActionResult typevars bound to it, and all imports.
Migration Plan (phased)
=======================
- Phase 1 - Foundation (review target): Make the three type bases self-contained; stand up the per-type framework (processor/runner/validator/monitor). Keep BaseAction temporarily as a deprecated shim so the ~46 unmigrated entities keep compiling.
- Phase 2 - Per-entity migration: Batch-migrate each XAction(BaseAction) entity. For each concrete action: classify as single/bulk/scope, move it onto the corresponding X{SingleEntity,Bulk,Scope}Action base, and implement the required methods (target_entity_id/target_element, targets, scope_\*). Route it through the matching per-type processor.
- Phase 3 - Removal: Once no references remain, delete BaseAction and its typevars.
Scope & Impact
==============
- Framework files: actions/processor/base.py, actions/validator/base.py, actions/monitors/monitor.py, actions/monitors/audit_log.py, actions/monitors/reporter.py, actions/action/{base,single_entity,bulk,scope,__init__}.py.
- Entity bases: ~46 XAction(BaseAction) classes across services/\*/actions/base.py and their concrete actions (several hundred classes total) - Phase 2.
- BaseRBACAction (actions/action/rbac.py) is independent of BaseAction and is unaffected.
Open Questions
==============
1. Monitor duplication: audit_log and reporter monitors are currently generic and registered once. Under a per-type framework, do we duplicate them per type, or keep a thin shared monitor that consumes only the common fields? (Affects how much of the contract truly must be per-type vs. shared.)
2. Service wiring: Each service constructs ActionProcessor(...) per action. Per-type processors change every service's processor wiring - confirm this is in Phase 2 scope.
Out of Scope
============
- Concrete action migration (Phase 2/3) is not part of this foundation work.
- No BaseSearchAction; search remains expressed via bulk/scope.
JIRA Issue: BA-6285
Contributor guide
Assessment
This issue has not been assessed yet.