Provider reactor — agent lifecycle orchestration #12

Open
opened 2026-04-16 11:30:32 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a user starting a turn, I want the ProviderReactor to automatically spawn the appropriate AI agent when a turn begins, stream its responses back as domain events, and handle completion/failure, so that the full turn lifecycle is automated.

Acceptance criteria

ProviderReactor (orchestration/reactors/provider_reactor.rs)

  • Implements Reactor trait
  • Holds references to ProviderManager and a weak ref to OrchestrationEngine
  • On TurnStarted event:
    1. Looks up the thread from the read model to determine the provider and model
    2. Gets the appropriate ProviderAdapter from ProviderManager
    3. Spawns a tokio::task that calls adapter.start_session()
    4. Iterates the returned event stream
    5. For each ProviderEvent, dispatches OrchestrationCommand::IngestProviderEvent back to the engine
  • On TurnInterrupted event:
    • Calls adapter.interrupt(thread_id) to kill the agent process
  • On stream end (agent finished): dispatches IngestProviderEvent(TurnComplete) if not already sent
  • On stream error: dispatches a TurnFailed event with the error message

IngestProviderEvent handling (in Decider)

  • ProviderEvent::TextDeltaMessageChunkReceived event
  • ProviderEvent::ToolCallMessageAppended(ToolCall) event
  • ProviderEvent::ToolResultMessageAppended(ToolResult) event
  • ProviderEvent::ThinkingDeltaMessageAppended(ThinkingBlock) or MessageChunkReceived
  • ProviderEvent::TurnCompleteTurnCompleted + ThreadStatusChanged(Idle)
  • ProviderEvent::ErrorTurnFailed + ThreadStatusChanged(Error(...))
  • ProviderEvent::AwaitingApprovalThreadStatusChanged(AwaitingApproval)

Tests

  • Test: TurnStarted triggers adapter.start_session with correct config
  • Test: ProviderEvent stream is correctly mapped to IngestProviderEvent commands
  • Test: TurnInterrupted calls adapter.interrupt()
  • Test: adapter error results in TurnFailed event
  • Use mock ProviderAdapter for all tests

Out of scope

  • Actual Claude/Codex CLI interaction (covered by provider adapter story)
  • Supervised mode UI approval (frontend story)

References

  • Spec §7.3 (ProviderReactor)
  • Spec Annexe A (Flux complet d'un turn)

Dependencies

  • Blocked by: #4 (decider extension surface), #7 (Reactor trait + engine), #11 (ProviderAdapter trait + ProviderManager)
  • Blocks: none (v0.1 leaf)
  • Branch off: issue-11-provider-adapter (then rebase onto issue-7-engine chain)
  • Full graph: #21
## User story As a **user starting a turn**, I want the ProviderReactor to automatically spawn the appropriate AI agent when a turn begins, stream its responses back as domain events, and handle completion/failure, so that the full turn lifecycle is automated. ## Acceptance criteria ### ProviderReactor (`orchestration/reactors/provider_reactor.rs`) - [ ] Implements `Reactor` trait - [ ] Holds references to `ProviderManager` and a weak ref to `OrchestrationEngine` - [ ] On `TurnStarted` event: 1. Looks up the thread from the read model to determine the provider and model 2. Gets the appropriate `ProviderAdapter` from `ProviderManager` 3. Spawns a `tokio::task` that calls `adapter.start_session()` 4. Iterates the returned event stream 5. For each `ProviderEvent`, dispatches `OrchestrationCommand::IngestProviderEvent` back to the engine - [ ] On `TurnInterrupted` event: - Calls `adapter.interrupt(thread_id)` to kill the agent process - [ ] On stream end (agent finished): dispatches `IngestProviderEvent(TurnComplete)` if not already sent - [ ] On stream error: dispatches a `TurnFailed` event with the error message ### IngestProviderEvent handling (in Decider) - [ ] `ProviderEvent::TextDelta` → `MessageChunkReceived` event - [ ] `ProviderEvent::ToolCall` → `MessageAppended(ToolCall)` event - [ ] `ProviderEvent::ToolResult` → `MessageAppended(ToolResult)` event - [ ] `ProviderEvent::ThinkingDelta` → `MessageAppended(ThinkingBlock)` or `MessageChunkReceived` - [ ] `ProviderEvent::TurnComplete` → `TurnCompleted` + `ThreadStatusChanged(Idle)` - [ ] `ProviderEvent::Error` → `TurnFailed` + `ThreadStatusChanged(Error(...))` - [ ] `ProviderEvent::AwaitingApproval` → `ThreadStatusChanged(AwaitingApproval)` ### Tests - [ ] Test: TurnStarted triggers adapter.start_session with correct config - [ ] Test: ProviderEvent stream is correctly mapped to IngestProviderEvent commands - [ ] Test: TurnInterrupted calls adapter.interrupt() - [ ] Test: adapter error results in TurnFailed event - [ ] Use mock ProviderAdapter for all tests ## Out of scope - Actual Claude/Codex CLI interaction (covered by provider adapter story) - Supervised mode UI approval (frontend story) ## References - Spec §7.3 (ProviderReactor) - Spec Annexe A (Flux complet d'un turn) ## Dependencies - **Blocked by:** #4 (decider extension surface), #7 (Reactor trait + engine), #11 (ProviderAdapter trait + ProviderManager) - **Blocks:** none (v0.1 leaf) - **Branch off:** `issue-11-provider-adapter` (then rebase onto `issue-7-engine` chain) - **Full graph:** #21
claude-desktop added this to the v0.1.0 milestone 2026-04-16 11:30:32 +00:00
Sign in to join this conversation.
No description provided.