State-Passed Architecture for Victor Orchestrator¶
Status: Active selective pattern within the service-first runtime
Last Reviewed: 2026-05-04
Priority: Apply only where explicit snapshots and transitions improve correctness
Overview¶
The state-passed architecture is a selective design pattern used in Victor for
decision, policy, and read-heavy seams that benefit from immutable snapshots
and explicit transitions.
It is not the canonical answer for every runtime concern. Effectful chat, tool,
provider, session, context, and recovery flows are service-owned. State-passed
is used where it improves correctness and testability without introducing a
second parallel runtime layer.
Problem Statement¶
The current AgentOrchestrator still has too much composition and
compatibility logic:
1. Large composition root: victor/agent/orchestrator.py is still ~4,593 LOC
2. Compatibility drag: Deprecated coordinator shims still exist for some seams
3. Testing difficulties: Some decision-heavy flows are still easier to test via explicit snapshots than host-object reach-through
4. Architecture risk: Without discipline, state-passed and service patterns can become parallel layers instead of complementary ones
Solution: Selective State-Passed Architecture¶
Use state-passed where the seam is fundamentally about analysis, classification,
policy, or recommended transitions:
- Input: Immutable ContextSnapshot of current state
- Output: CoordinatorResult with explicit StateTransition objects
- No hidden mutation: Decisions are explicit and testable
- No blanket rewrite: Effectful runtime flows remain service-owned
Current Entry Points¶
Use the concrete migration surfaces that are already wired into the runtime.
| Need | Preferred surface | Notes |
|---|---|---|
| Chat execution | ChatService or OrchestrationFacade.chat_service |
Canonical effectful runtime |
| Tool execution | ToolService or OrchestrationFacade.tool_service |
Canonical effectful runtime |
| Session lifecycle | SessionService or OrchestrationFacade.session_service |
Canonical effectful runtime |
| Context management | ContextService or OrchestrationFacade.context_service |
Canonical effectful runtime |
| Provider management | ProviderService or OrchestrationFacade.provider_service |
Canonical effectful runtime |
| Recovery and resilience | RecoveryService or OrchestrationFacade.recovery_service |
Canonical effectful runtime |
| Read-only exploration helper | victor.agent.coordinators.ExplorationCoordinator |
Legacy helper for direct exploration calls; use the state-passed wrapper for explicit transitions |
| State-passed exploration | victor.agent.coordinators.ExplorationStatePassedCoordinator |
Snapshot/transition wrapper |
| State-passed system prompt logic | victor.agent.coordinators.SystemPromptStatePassedCoordinator |
Prefer over direct shim imports |
| State-passed safety checks | victor.agent.coordinators.SafetyStatePassedCoordinator |
Prefer over coordinator shim usage |
| State-passed coordination recommendation | victor.agent.coordinators.CoordinationStatePassedCoordinator |
Prefer over legacy coordination heuristics |
Deprecated chat shims are compatibility-only. In particular,
ChatCoordinator.stream_chat() now forwards in this order:
1. bound ChatService.stream_chat()
2. orchestrator _get_service_streaming_runtime()
3. legacy _stream_chat_runtime hook
That legacy hook remains only as a fallback for older integrations and should
not be used as the primary runtime surface in new code.
from victor.agent.coordinators import (
ExplorationCoordinator,
ExplorationStatePassedCoordinator,
SafetyStatePassedCoordinator,
SystemPromptStatePassedCoordinator,
)
from victor.agent.facades import OrchestrationFacade
facade = OrchestrationFacade(
chat_service=chat_service,
tool_service=tool_service,
session_service=session_service,
exploration_state_passed=ExplorationStatePassedCoordinator(),
system_prompt_state_passed=SystemPromptStatePassedCoordinator(
task_analyzer=task_analyzer
),
safety_state_passed=SafetyStatePassedCoordinator(),
)
findings = await ExplorationCoordinator().explore_parallel(
"Trace the failing import boundary",
project_root=project_root,
)
Core Components¶
1. ContextSnapshot¶
Immutable snapshot of orchestrator state at a point in time.
@dataclass(frozen=True)
class ContextSnapshot:
"""Immutable snapshot of orchestrator state."""
# Core conversation state
messages: tuple[Message, ...]
session_id: str
conversation_stage: str
# Configuration
settings: Settings
model: str
provider: str
max_tokens: int
temperature: float
# State (key-value store)
conversation_state: Dict[str, Any]
session_state: Dict[str, Any]
# Metadata
observed_files: tuple[str, ...]
capabilities: Dict[str, Any]
Key Features:
- Frozen: Cannot be modified (prevents accidental mutation)
- Tuples: Collections are tuples (not lists) for immutability
- Dict copies: State dictionaries are copied (prevents shared mutation)
Usage:
# Read from context
message_count = context.message_count
stage = context.conversation_stage
value = context.get_state("key", default=None)
# Check capabilities
if context.has_capability("planning_mode"):
# ...
2. StateTransition¶
Encapsulates a single state change or side effect.
@dataclass
class StateTransition:
"""Encapsulates a state change or side effect."""
transition_type: TransitionType
data: Dict[str, Any]
metadata: Dict[str, Any] = field(default_factory=dict)
Transition Types:
- ADD_MESSAGE: Add a message to conversation
- UPDATE_STATE: Update a key-value pair in state
- DELETE_STATE: Remove a key from state
- EXECUTE_TOOL: Request tool execution
- UPDATE_CAPABILITY: Update a capability flag
- UPDATE_STAGE: Update conversation stage
- CREATE_SESSION / CLOSE_SESSION: Session management
Usage:
# Create transitions
transition1 = StateTransition(
transition_type=TransitionType.UPDATE_STATE,
data={"key": "plan_steps", "value": steps, "scope": "conversation"}
)
transition2 = StateTransition(
transition_type=TransitionType.EXECUTE_TOOL,
data={"tool_name": "list_files", "arguments": {"path": "."}}
)
3. TransitionBatch¶
Batches multiple transitions for atomic application.
@dataclass
class TransitionBatch:
"""A batch of transitions to be applied atomically."""
transitions: List[StateTransition] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
Helper Methods:
batch = TransitionBatch()
# Method chaining for fluent API
batch.update_state("key", "value") \
.add_message(message) \
.execute_tool("tool_name", {"arg": "value"})
# Extend with other batches
batch.extend(other_batch)
4. CoordinatorResult¶
Combines transitions with metadata about coordinator decisions.
@dataclass
class CoordinatorResult:
"""Result of a coordinator operation."""
transitions: TransitionBatch
reasoning: Optional[str] = None
confidence: float = 1.0
metadata: Dict[str, Any] = field(default_factory=dict)
should_continue: bool = True
handoff_to: Optional[str] = None
Factory Methods:
# No-op result
result = CoordinatorResult.no_op(reasoning="Nothing to do")
# Result with transitions
result = CoordinatorResult.transitions_only(
transition1, transition2,
reasoning="Applied updates"
)
5. TransitionApplier¶
Applies transitions to the actual orchestrator.
class TransitionApplier:
"""Applies state transitions to an orchestrator."""
async def apply(self, transition: StateTransition) -> None:
"""Apply a single transition."""
async def apply_batch(self, batch: TransitionBatch) -> None:
"""Apply a batch of transitions atomically."""
6. create_snapshot()¶
Utility function to create snapshots from orchestrator.
def create_snapshot(orchestrator: Any) -> ContextSnapshot:
"""Create a ContextSnapshot from an orchestrator."""
Migration Pattern¶
Before (Current Pattern)¶
class ExampleCoordinator:
def __init__(self, orchestrator):
self._orchestrator = orchestrator # Holds reference
async def process(self, user_message):
# Direct state access and mutation
messages = self._orchestrator.messages
self._orchestrator.conversation_state["key"] = "value"
await self._orchestrator.execute_tool("tool", {})
Issues:
- Tight coupling to orchestrator
- Direct state mutation
- Hard to test (needs full orchestrator mock)
After (State-Passed Pattern)¶
class ExampleCoordinator:
# No __init__ needed (or only config)
async def process(self, context: ContextSnapshot, user_message: str) -> CoordinatorResult:
# Pure function: read from context, return transitions
messages = context.messages # Read from immutable snapshot
batch = TransitionBatch()
batch.update_state("key", "value", scope="conversation")
batch.execute_tool("tool", {})
return CoordinatorResult(
transitions=batch,
reasoning="Updated state and executed tool",
)
Benefits:
- No orchestrator reference
- No direct state mutation
- Easy to test (just create ContextSnapshot)
Integration Example¶
Orchestrator Integration¶
class AgentOrchestrator:
async def process_with_coordinator(self, user_message: str):
from victor.agent.coordinators.state_context import (
create_snapshot,
TransitionApplier,
)
# 1. Create coordinator (no orchestrator reference)
coordinator = ExampleStatePassedCoordinator()
# 2. Create snapshot
snapshot = create_snapshot(self)
# 3. Call coordinator (pure function)
result = await coordinator.process(snapshot, user_message)
# 4. Apply transitions
applier = TransitionApplier(self)
await applier.apply_batch(result.transitions)
# 5. Use result metadata
if result.reasoning:
logger.info(f"Coordinator: {result.reasoning}")
if not result.should_continue:
return # Stop processing
if result.handoff_to:
# Hand off to next coordinator
await self._handoff(result.handoff_to, snapshot, user_message)
Testing Benefits¶
Before (Needs Full Mock)¶
def test_coordinator():
# Need complex mock setup
orchestrator = MagicMock()
orchestrator.messages = [msg1, msg2]
orchestrator.conversation_state = {}
orchestrator.execute_tool = AsyncMock()
coordinator = ExampleCoordinator(orchestrator)
await coordinator.process("test")
# Verify orchestrator was called
orchestrator.execute_tool.assert_called_once()
After (Simple Snapshot)¶
def test_coordinator():
# Just create a snapshot
snapshot = ContextSnapshot(
messages=(msg1, msg2),
session_id="test",
conversation_stage="initial",
settings=Settings(),
model="test",
provider="test",
max_tokens=4096,
temperature=0.7,
conversation_state={},
session_state={},
observed_files=(),
capabilities={},
)
coordinator = ExampleStatePassedCoordinator()
result = await coordinator.process(snapshot, "test")
# Verify transitions (no orchestrator mock needed!)
assert not result.transitions.is_empty()
assert result.confidence > 0.5
File Structure¶
victor/agent/coordinators/
├── state_context.py # Core abstractions
├── example_state_passed_coordinator.py # Example implementation
├── chat_coordinator.py # Deprecated shim over service-first runtime
├── planning_coordinator.py # [TODO: Refactor]
├── execution_coordinator.py # [TODO: Refactor]
└── sync_chat_coordinator.py # Deprecated shim over service-first runtime
tests/unit/agent/coordinators/
└── test_state_context.py # 34 comprehensive tests
Migration Checklist¶
Phase 1: Foundation (✅ COMPLETE)¶
- Create
state_context.pywith core abstractions - Create
example_state_passed_coordinator.pytemplate - Create comprehensive unit tests (34 tests)
- Document the pattern
Phase 2: Gradual Migration (UPDATED 2026-04-27)¶
- Collapse
chat_coordinator.pyinto a deprecated shim overChatService - Canonical runtime ownership moved to
ChatService - Deprecated compatibility surface now delegates to service/orchestrator public entrypoints
- Architecture tests guard against reintroducing local loop ownership
- Refactor
planning_coordinator.py - Refactor
execution_coordinator.py - Collapse
sync_chat_coordinator.pyinto a deprecated shim overChatService - Canonical runtime ownership moved to
ChatService - Planning path now routes through
ChatService.chat(..., use_planning=True)or orchestrator public chat - Update orchestrator integration points
Phase 3: Validation (FUTURE)¶
- Run full test suite
- Integration testing
- Performance benchmarking
- Documentation updates
Design Principles¶
1. Immutability¶
- Snapshots are frozen (cannot be modified)
- Collections are tuples (not lists)
- State dictionaries are copied
2. Explicit Transitions¶
- All state changes are explicit (no hidden mutations)
- Transitions are validated on creation
- Atomic batch application
3. Testability¶
- Coordinators are pure functions
- No orchestrator mocks needed
- Simple snapshot construction
4. Gradual Migration¶
- Old and new patterns can coexist
- No breaking changes required
- Incremental adoption possible
FAQ¶
Q: Why not just use protocols?¶
A: Protocols still allow direct state mutation. State-passed ensures coordinators are pure functions with no side effects.
Q: Won't this be slower?¶
A: Negligible impact. Snapshot creation is fast (copies dicts, tuples references). The benefit in testability and maintainability far outweighs minimal overhead.
Q: Do I need to refactor all coordinators at once?¶
A: No! The old and new patterns can coexist. Migrate incrementally, one coordinator at a time.
Q: How do I handle async operations (tool execution)?¶
A: Return an EXECUTE_TOOL transition. The orchestrator will apply it and handle the async execution.
Q: What about nested coordinator calls?¶
A: Pass the same snapshot. Each coordinator returns its own transitions. The orchestrator merges and applies them.
References¶
- Gemini Feedback: Verified claim about 3,915 LOC orchestrator complexity
- Implementation:
victor/agent/coordinators/state_context.py - Example:
victor/agent/coordinators/example_state_passed_coordinator.py - Tests:
tests/unit/agent/coordinators/test_state_context.py
Status Summary¶
| Concern | Status | Notes |
|---|---|---|
| Core abstractions | ✅ Complete | Context snapshot, transitions, and result types exist |
| Example coordinator | ✅ Complete | Reference implementation remains useful |
| Service-owned effectful runtime | ✅ Canonical | Chat, tool, session, context, provider, and recovery stay service-owned |
| Exploration / system prompt / safety | ✅ Canonical selective seams | State-passed is the preferred pattern here |
| Coordination recommendation | ✅ Canonical selective seam | coordination_state_passed.py |
| Blanket coordinator rewrites | 🚫 Not the goal | Do not create a second parallel runtime layer |
| Remaining orchestrator shrink work | ⏳ In progress | AgentOrchestrator is still ~4,593 LOC |
Overall Progress: State-passed is now an established selective pattern inside
the broader service-first runtime. Future work should target seams that still
benefit from explicit transitions, not re-migrate domains already cleanly owned
by services.
Next Steps: Keep service-owned domains service-owned, continue deleting dead
compatibility paths, and use example_state_passed_coordinator.py as the
reference when a remaining decision seam genuinely benefits from explicit
snapshot/transition modeling.