Skip to content

feat(orchestrator): define ExecutionState and TransitionTrigger enums#1225

Open
geoffjay wants to merge 2 commits into
mainfrom
issue-1217
Open

feat(orchestrator): define ExecutionState and TransitionTrigger enums#1225
geoffjay wants to merge 2 commits into
mainfrom
issue-1217

Conversation

@geoffjay
Copy link
Copy Markdown
Owner

Implements the formal execution state machine required as the foundation for the #1216 epic.

What was added

ExecutionState enum (types.rs) — 7 states unifying the current split between AgentStatus (persisted) and ActivityState (in-memory):

State Meaning
Pending Record created, backend not yet spawned
Starting Session spawned, waiting for process to connect
Idle Connected, waiting for input
Busy Actively processing a prompt
Suspended Temporarily suspended (e.g. context-clear)
Stopped Exited cleanly
Failed Crashed or errored
  • transition_to(target) — validated transition; returns Err on illegal moves
  • can_transition_to(target) — boolean predicate
  • is_terminal() / is_active() — state category helpers
  • to_agent_status() — maps to existing AgentStatus (backward compat)
  • to_activity_state() — maps to existing ActivityState (backward compat)
  • Display, FromStr, Serialize, Deserialize

TransitionTrigger enum — 11 variants recording why a transition happened (Spawn, Connected, Disconnected, PromptReceived, ResultReceived, ContextCleared, UserTerminated, ProcessExited, ProcessCrashed, Restart, Reconciliation). Full Display, FromStr, Serialize, Deserialize.

Tests

110 tests pass (all existing + new):

  • All valid transitions for every state
  • All invalid transitions (including self-transitions) rejected
  • Display/FromStr round-trips for both enums
  • JSON snake_case serialization verified
  • to_agent_status() and to_activity_state() backward-compat mappings

Closes #1217

geoffjay added 2 commits May 12, 2026 17:17
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.
@geoffjay geoffjay added the review-agent Used to invoke a review by an agent tracking this label label May 13, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.77%. Comparing base (31940cb) to head (1ba0623).

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           
Flag Coverage Δ
frontend 63.77% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Owner Author

@geoffjay geoffjay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 correct AgentStatus variant ✓
  • to_activity_state(): BusyBusy, all others → Idle ✓ — the doc comment correctly acknowledges the semantic simplification for Stopped/FailedIdle

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 HashMap keys 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 self

transition_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_to as a pure predicate separate from transition_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 / serde consistency: all three use the same lowercase snake_case strings, so a value round-trips correctly through to_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 the Display impl, 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.

@geoffjay geoffjay added merge-queue Approved by reviewer, queued for merge by conductor and removed review-agent Used to invoke a review by an agent tracking this label labels May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-queue Approved by reviewer, queued for merge by conductor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Define formal execution state machine with validated transitions

1 participant