ADR-049: Conversational State and Hierarchical Intent Architecture

Status: Accepted Date: 2026-01-09 Accepted: 2026-01-26 Issue: #490 FTUX-PORTFOLIO Implementation: #427 MUX-IMPLEMENT-CONVERSE-MODEL Author: Lead Developer (Claude Code Opus) Approver: PM (xian), PPM, Chief Architect

Context

During implementation of portfolio onboarding (Issue #490), we discovered a fundamental architectural gap: Piper lacked “conversational state” - the ability to maintain control of a guided conversation once it begins.

The Problem

When a user starts the onboarding flow:

  1. Turn 1 (Greeting): User says “Hello” → Piper correctly triggers onboarding prompt
  2. Turn 2 (Project Info): User says “My main project is called Piper Morgan” → BUG: Message gets re-classified as IDENTITY intent (because “Piper Morgan” matches identity patterns), returning the identity response instead of continuing onboarding

The root cause: Intent classification happens every turn, with no awareness that a guided process is in progress.

Observed User Experience Issues

Decision

Adopt a two-tier intent architecture:

Tier 1: High-Level Conversational State (Process-Level Intent)

Represents the user’s active engagement with a structured process:

High-level state is:

Tier 2: Turn-Level Intent (Message-Level Classification)

Represents the micro-intent within a single message:

Turn-level intent is:

Implementation Pattern

async def process_intent(self, message: str, user_id: str, session_id: str):
    # TIER 1: Check for active conversational state FIRST
    if user_id:
        active_process = await self._check_active_process(user_id, session_id)
        if active_process:
            # Route directly to process handler - bypass classification
            return await active_process.handle_turn(message)

    # TIER 2: No active process - perform normal classification
    classified_intent = await self._classify_message(message)

    # Classification may START a new process (e.g., greeting → onboarding)
    return await self._route_to_handler(classified_intent)

Process Priority Check Order

  1. Active onboarding session (user is setting up portfolio)
  2. Active standup session (user is doing daily check-in)
  3. Active planning session (user is in planning mode)
  4. Pending clarification (Piper asked a question)
  5. No active process → perform classification

State Transitions

[No Process] --(greeting + new user)--> [Onboarding Active]
[Onboarding Active] --(user confirms)--> [Onboarding Complete] --> [No Process]
[Onboarding Active] --(user declines)--> [Onboarding Declined] --> [No Process]
[Onboarding Active] --(timeout)--> [Onboarding Expired] --> [No Process]

Rationale

Why Process-Level Takes Priority

  1. User expectation: When I agree to do something, I expect continuity
  2. UX principle: Guided flows should feel guided, not interrupted
  3. Pattern precedent: Standup assistant (Epic #242) already works this way
  4. Technical simplicity: Single check at start vs. complex re-classification

Why Not Just “Better Classification”?

We considered improving the intent classifier to detect “user is continuing onboarding” but rejected this because:

  1. Semantic ambiguity: “My project is Piper Morgan” could legitimately be identity OR project info
  2. Fragile patterns: Any pattern-based approach would have false positives
  3. Wrong abstraction: The problem isn’t classification accuracy, it’s architectural flow
  4. LLM-dependent: Would require expensive LLM calls for context-aware classification

Singleton Manager Pattern

The PortfolioOnboardingManager uses a module-level singleton to persist session state across HTTP requests. This pattern:

Warning: Creating new PortfolioOnboardingManager() instances loses state. Always use the singleton accessor.

Consequences

Positive

Negative

Risks and Mitigations

Risk Mitigation
Memory growth from abandoned sessions cleanup_expired() runs on configurable interval (default 30 min)
Process “traps” user Explicit decline patterns always work; timeout releases
Classification never runs during process Deliberate - process handler interprets messages contextually
Testing complexity E2E tests validate real user flows (Pattern-045 compliance)

Implementation Notes

Generalized Architecture (January 2026)

The pattern has been generalized into the ProcessRegistry system:

New Files:

Modified Files:

Key Concepts:

Test Coverage:

Original Files (MVP)

Pattern Compliance

This ADR aligns with:

Future Vision

User-defined guided processes (analogous to Claude skills) could extend this architecture.

Review History

Date Reviewer Decision
2026-01-09 PM (xian) Proposed
2026-01-26 PPM, Chief Architect Approved for MVP implementation
2026-01-26 PM (xian) Accepted - implemented in #427