feat(orchestrator): define ExecutionState and TransitionTrigger enums#1225
feat(orchestrator): define ExecutionState and TransitionTrigger enums#1225geoffjay wants to merge 2 commits into
Conversation
Add a unified ExecutionState enum (Pending, Starting, Idle, Busy, Suspended, Stopped, Failed) with a validated transition map and helper methods (transition_to, can_transition_to, is_terminal, is_active). Add backward-compatibility shims to_agent_status() and to_activity_state() so existing API and WebSocket consumers are unaffected. Add TransitionTrigger enum (11 variants) for recording why a transition happened, with Display, FromStr, Serialize, and Deserialize. Both types include full unit-test coverage: all valid transitions, all invalid transitions, self-transition rejection, Display/FromStr round-trips, JSON serialization, and backward-compat mapping.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1225 +/- ##
=======================================
Coverage 63.77% 63.77%
=======================================
Files 173 173
Lines 7733 7733
Branches 2566 2566
=======================================
Hits 4932 4932
Misses 2780 2780
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
geoffjay
left a comment
There was a problem hiding this comment.
Review: feat(orchestrator): define ExecutionState and TransitionTrigger enums
Stack Position
Branch issue-1217 is based directly on main — no parent PR to review first. This is the foundation of the #1216 epic. Four downstream issues (#1219 — transition history table, #1221 — API, #1222 — tests, #1223 — docs) are all blocked on this landing.
Transition Table: Verified Correct
The can_transition_to implementation encodes exactly the documented table — every legal pair is present, no extras:
Pending → Starting, Failed ✓
Starting → Idle, Failed ✓
Idle → Busy, Suspended, Stopped, Failed ✓
Busy → Idle, Suspended, Stopped, Failed ✓
Suspended → Idle, Busy, Stopped, Failed ✓
Stopped → Starting ✓ (restart)
Failed → Starting ✓ (restart)
Self-transitions are correctly rejected (confirmed by test_invalid_self_transitions).
Backward-Compat Mappings: Verified Against Existing Types
Checked AgentStatus (4 variants: Pending, Running, Stopped, Failed) and ActivityState (2 variants: Idle, Busy) on main:
to_agent_status(): all 7 mappings land on the correctAgentStatusvariant ✓to_activity_state():Busy→Busy, all others →Idle✓ — the doc comment correctly acknowledges the semantic simplification forStopped/Failed→Idle
Non-Blocking: Missing Hash and Copy Derives
Both enums are fieldless unit variants — they are eligible for Copy and Hash. Without them:
- They can't be used as
HashMapkeys directly - Call sites must
clone()when the enum is needed in multiple places (visible in the tests:terminal.clone().transition_to(...))
Suggest adding #[derive(Hash, Copy)] to both ExecutionState and TransitionTrigger. Downstream crates (#1219 — which stores these in a DB row — would benefit from Hash for de-duplication sets).
Non-Blocking: transition_to Success Path Untested
Every transition test checks only is_ok() / is_err(). None of them unwrap the Ok value and assert the returned state:
// current:
assert!(ExecutionState::Pending.transition_to(ExecutionState::Starting).is_ok());
// suggested addition:
let new = ExecutionState::Pending.transition_to(ExecutionState::Starting).unwrap();
assert_eq!(new, ExecutionState::Starting);The implementation (Ok(target)) is obviously correct, but this gap means a refactor that accidentally returns Ok(self) instead of Ok(target) would not be caught. One or two spot-checks would close this.
Non-Blocking: Unnecessary .clone() on &self Receiver
In test_invalid_transitions_from_terminal_states and test_invalid_self_transitions:
assert!(terminal.clone().transition_to(ExecutionState::Pending).is_err());
// ^^^^^^^
// unnecessary: transition_to takes &self, not selftransition_to is declared pub fn transition_to(&self, target: ExecutionState) — self is borrowed, so terminal is not consumed. The .clone() calls are harmless but misleading (they imply ownership transfer where none occurs).
What's Working Well
can_transition_toas a pure predicate separate fromtransition_to: callers can check feasibility without committing, which will be useful for the reconciliation path in #1221.is_terminal()/is_active()helpers: cleanly cover all 7 variants with exhaustive assertions in tests — no variant is missing from either predicate.Display/FromStr/serdeconsistency: all three use the same lowercase snake_case strings, so a value round-trips correctly throughto_string(),parse(), JSON, and SQLite text columns.- Test count: 69 → 93 tests in
types.rs; every valid and invalid transition is covered, plus all serialization and mapping paths. - Error message in
transition_to:"Invalid execution state transition: {} → {}"uses theDisplayimpl, which means it's human-readable in log output immediately.
Summary
No blocking issues. The transition table is correctly encoded, the backward-compat layer is verified against the existing types, and the test suite is thorough. Three non-blocking suggestions: add Hash/Copy derives, add one success-path assertion to transition_to tests, and drop the unnecessary .clone() calls in two test functions. Approving and adding to merge queue — downstream #1219, #1221, #1222, #1223 are waiting on this.
Implements the formal execution state machine required as the foundation for the #1216 epic.
What was added
ExecutionStateenum (types.rs) — 7 states unifying the current split betweenAgentStatus(persisted) andActivityState(in-memory):PendingStartingIdleBusySuspendedStoppedFailedtransition_to(target)— validated transition; returnsErron illegal movescan_transition_to(target)— boolean predicateis_terminal()/is_active()— state category helpersto_agent_status()— maps to existingAgentStatus(backward compat)to_activity_state()— maps to existingActivityState(backward compat)Display,FromStr,Serialize,DeserializeTransitionTriggerenum — 11 variants recording why a transition happened (Spawn,Connected,Disconnected,PromptReceived,ResultReceived,ContextCleared,UserTerminated,ProcessExited,ProcessCrashed,Restart,Reconciliation). FullDisplay,FromStr,Serialize,Deserialize.Tests
110 tests pass (all existing + new):
to_agent_status()andto_activity_state()backward-compat mappingsCloses #1217