-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathAgentStateManager.swift
More file actions
118 lines (97 loc) · 3.57 KB
/
Copy pathAgentStateManager.swift
File metadata and controls
118 lines (97 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import Foundation
enum AgentStateSignal {
case vadScore(Double)
case agentStartedSpeaking
case agentStoppedSpeaking
case agentResponse
case userTranscript
case interruption
case agentToolRequest
case agentToolResponse
}
private typealias TimerTask = Task<Void, Never>
@MainActor
final class AgentStateManager {
private(set) var currentState: AgentState = .listening
var onStateChange: ((AgentState) -> Void)?
private let configuration: AgentStateConfiguration
private var isUserSpeaking = false
private var isAgentSpeaking = false
private var speechTimer: TimerTask?
private var silenceTimer: TimerTask?
private var speakingTimer: TimerTask?
init(configuration: AgentStateConfiguration) {
self.configuration = configuration
}
func processSignal(_ signal: AgentStateSignal) {
switch signal {
case let .vadScore(score):
handleVadScore(score)
case .agentStartedSpeaking:
isAgentSpeaking = true
cancelTimer(&speakingTimer)
transitionTo(.speaking)
case .agentStoppedSpeaking:
isAgentSpeaking = false
scheduleTimer(&speakingTimer, delay: configuration.speakingToListeningDelay) { [weak self] in
guard let self, !self.isAgentSpeaking, currentState == .speaking else { return }
transitionTo(.listening)
}
case .agentResponse:
cancelTimer(&speakingTimer)
transitionTo(.speaking)
case .userTranscript:
if currentState != .speaking { transitionTo(.thinking) }
case .interruption:
cancelAllTimers()
isAgentSpeaking = false
transitionTo(.listening)
case .agentToolRequest:
transitionTo(.thinking)
case .agentToolResponse:
transitionTo(.listening)
}
}
private func handleVadScore(_ score: Double) {
let wasSpeaking = isUserSpeaking
let isSpeakingNow = score >= configuration.vadSpeakingThreshold
isUserSpeaking = isSpeakingNow
if isSpeakingNow, !wasSpeaking {
cancelTimer(&silenceTimer)
scheduleTimer(&speechTimer, delay: configuration.minSpeechDuration) { [weak self] in
guard let self, isUserSpeaking else { return }
transitionTo(.listening)
}
} else if !isSpeakingNow, wasSpeaking {
cancelTimer(&speechTimer)
scheduleTimer(&silenceTimer, delay: configuration.minSilenceDuration) { [weak self] in
guard let self, !self.isUserSpeaking, currentState != .speaking else { return }
transitionTo(.thinking)
}
} else if isSpeakingNow {
cancelTimer(&silenceTimer)
}
}
private func transitionTo(_ newState: AgentState) {
guard newState != currentState else { return }
currentState = newState
onStateChange?(newState)
}
private func scheduleTimer(_ timer: inout TimerTask?, delay: TimeInterval, action: @escaping @MainActor () -> Void) {
timer?.cancel()
timer = Task { @MainActor in
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
guard !Task.isCancelled else { return }
action()
}
}
private func cancelTimer(_ timer: inout TimerTask?) {
timer?.cancel()
timer = nil
}
private func cancelAllTimers() {
cancelTimer(&speechTimer)
cancelTimer(&silenceTimer)
cancelTimer(&speakingTimer)
}
}