Skip to content

D-10: AgentChatFeature — TCA reducer, state, actions, dedup #260

@kirich1409

Description

@kirich1409

Description

Implement the AgentChatFeature TCA reducer that consumes AgentStreamEvents from an AgentChatSession and maintains the dialogue transcript state.

Spec: Epic #250 §4.1, §4.3 (data flow); docs/architecture/dialogue-events.md §3 (Mapping on UI).

Scope

File

MacApp/Packages/AgentChat/Sources/AgentChat/Reducer/AgentChatFeature.swift

State

```swift
@ObservableState
public struct State: Equatable {
public var sessionID: SessionID
public var agentKind: AgentKind
public var status: AgentChatStatus
public var transcript: IdentifiedArrayOf
public var toolCalls: IdentifiedArrayOf
public var pendingApproval: ApprovalRequest?
public var inputDraft: String
public var sessionInit: SessionInitInfo?
public var sessionResult: SessionResult?
public var rateLimit: RateLimitInfo?
public var apiRetry: RetryInfo?
public var compactBoundaries: [CompactBoundary] // dividers in transcript
public var scrollAnchorAtBottom: Bool // for auto-scroll logic
public var costAlertShown: Bool
}
```

Actions

```swift
public enum Action {
case onAppear
case inputChanged(String)
case sendMessage
case interruptTurn
case scrollReachedBottom(Bool)
case dismissApproval
case approvalDecision(ApprovalRequest, ApprovalDecision)
case openStreamInspector

// Internal
case _streamEvent(AgentStreamEvent)
case _sessionStatusChanged(AgentChatStatus)
case _startSession(any AgentChatSession)

}
```

Reducer logic

  • onAppear → start session via Effect, subscribe to its transcript stream.
  • _streamEvent → dispatch by case to state mutations:
    • sessionInit(info)state.sessionInit = info, status = .idle.
    • messageStarted(id, role, model) → append new AgentMessage with empty content.
    • textDelta(messageID, text) → locate message, append to last .text segment (create if absent). Throttle 30fps via Effect.debounce (handled at view layer — reducer is immediate).
    • thinkingStarted(messageID, index) → append .thinking segment, collapsed.
    • thinkingDelta(messageID, index, text) → append to thinking buffer (no re-render signal — view reads on expand).
    • toolCallStarted(id, name, kind, messageID) → insert into toolCalls, append .toolCall(id) marker to parent message content.
    • toolCallInputReady(id, input) → update existing ToolCall.input.
    • toolCallCompleted(id, output) → status = .completed.
    • toolCallFailed(id, error) → status = .failed.
    • apiRetry(...)state.apiRetry = .init(...); set status = .error transient if attempt ≥ 3.
    • rateLimitStatus(info)state.rateLimit = info. If rejected → status = .error.
    • compactBoundary(...) → append to compactBoundaries.
    • sessionEnd(result)state.sessionResult = result, status = .idle (or .error if is_error).
    • messageCompleted(id, stopReason, usage) → finalize message, update AgentMessage.usage / stopReason.
    • unknownEvent(raw) → log to Stream Inspector buffer, increment metric.
  • sendMessage → dispatch session.send(.text(state.inputDraft)), clear draft.
  • interruptTurn → dispatch session.interrupt().
  • Dedup: handled at parser level (D-8); reducer trusts incoming events are unique.

Dependency injection

Via @Dependency(\\.dialogueSessionFactory) — a factory (SessionID, TerminalSession, AgentKind) -> any AgentChatSession. Test injection via test dependency.

Acceptance Criteria

  • AgentChatFeature reducer compiles with all actions/state defined.
  • Stream event → state mapping covers all 15+ AgentStreamEvent variants.
  • Unit tests via TestStore — feed a sequence of events, verify final state (minimum: text stream, tool call lifecycle, api_retry, session_end, unknown event).
  • IdentifiedArrayOf<AgentMessage> O(1) updates for textDelta; no full-array rewrite.
  • costAlertShown triggers when sessionResult.total_cost_usd exceeds threshold from Settings.
  • Swift 6 strict concurrency clean.

Relationships

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions