Skip to content

Commit daa029c

Browse files
committed
feat: migrate to one AgentState value across useAgent + useVoiceAssistant
- migrate unset -> connecting in useAgent - update useVoiceAssistant to point to to the useAgent version of the states
1 parent 358c4f0 commit daa029c

File tree

2 files changed

+56
-60
lines changed

2 files changed

+56
-60
lines changed

packages/react/src/hooks/useAgent.ts

Lines changed: 55 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,30 @@ import { ParticipantAgentAttributes, TrackReference } from '@livekit/components-
77

88
import { useParticipantTracks } from './useParticipantTracks';
99
import { useRemoteParticipants } from './useRemoteParticipants';
10-
import { AgentState as LegacyAgentState } from './useVoiceAssistant';
1110
import { ConversationInstance } from './useConversationWith';
1211

1312
// FIXME: make this 10 seconds once room dispatch booting info is discoverable
1413
const DEFAULT_AGENT_CONNECT_TIMEOUT_MILLISECONDS = 20_000;
1514

16-
/** State representing the current status of the agent, whether it is ready for speach, etc */
17-
export type AgentStateNew = 'unset' | 'initializing' | 'failed' | 'idle' | 'listening' | 'thinking' | 'speaking';
15+
/** @see https://github.com/livekit/agents/blob/65170238db197f62f479eb7aaef1c0e18bfad6e7/livekit-agents/livekit/agents/voice/events.py#L97 */
16+
type AgentSdkStates = 'initializing' | 'idle' | 'listening' | 'thinking' | 'speaking';
17+
18+
/**
19+
* State representing the current status of the agent, whether it is ready for speach, etc
20+
*
21+
* For most agents (which have the preconnect audio buffer feature enabled), this is the lifecycle:
22+
* connecting -> listening -> listening/thinking/speaking
23+
*
24+
* For agents without the preconnect audio feature enabled:
25+
* connecting -> initializing -> idle/listening/thinking/speaking
26+
*
27+
* If an agent fails to connect:
28+
* connecting -> listening/initializing -> failed
29+
*
30+
* Legacy useVoiceAssistant hook:
31+
* disconnected -> connecting -> initializing -> listening/thinking/speaking
32+
* */
33+
export type AgentState = 'disconnected' | 'connecting' | 'failed' | AgentSdkStates;
1834

1935
export enum AgentEvent {
2036
CameraChanged = 'cameraChanged',
@@ -27,7 +43,7 @@ export type AgentCallbacks = {
2743
[AgentEvent.CameraChanged]: (newTrack: TrackReference | null) => void;
2844
[AgentEvent.MicrophoneChanged]: (newTrack: TrackReference | null) => void;
2945
[AgentEvent.AttributesChanged]: (newAttributes: Record<string, string>) => void;
30-
[AgentEvent.StateChanged]: (newAgentState: AgentStateNew) => void;
46+
[AgentEvent.StateChanged]: (newAgentState: AgentState) => void;
3147
};
3248

3349
type AgentInstanceCommon = {
@@ -41,11 +57,6 @@ type AgentInstanceCommon = {
4157

4258
agentParticipant: RemoteParticipant | null;
4359
workerParticipant: RemoteParticipant | null;
44-
45-
/** A computed version of the old {@link AgentState} value returned by {@link useVoiceAssistant}
46-
* @deprecated Use conversation.connectionState / agent.lifecycleState if at all possible
47-
*/
48-
legacyAgentState: LegacyAgentState;
4960
};
5061
};
5162

@@ -61,7 +72,18 @@ type AgentStateAvailable = AgentInstanceCommon & {
6172
};
6273

6374
type AgentStateUnAvailable = AgentInstanceCommon & {
64-
state: "unset" | "initializing" | "idle";
75+
state: "initializing" | "idle";
76+
failureReasons: null;
77+
78+
/** Is the agent ready for user interaction? */
79+
isAvailable: false;
80+
81+
cameraTrack: TrackReference | null;
82+
microphoneTrack: TrackReference | null;
83+
};
84+
85+
type AgentStateConnecting = AgentInstanceCommon & {
86+
state: "connecting";
6587
failureReasons: null;
6688

6789
/** Is the agent ready for user interaction? */
@@ -93,10 +115,10 @@ type AgentActions = {
93115
waitUntilMicrophone: (signal?: AbortSignal) => Promise<TrackReference>;
94116
};
95117

96-
type AgentStateCases = AgentStateAvailable | AgentStateUnAvailable | AgentStateFailed;
118+
type AgentStateCases = AgentStateConnecting | AgentStateAvailable | AgentStateUnAvailable | AgentStateFailed;
97119
export type AgentInstance = AgentStateCases & AgentActions;
98120

99-
const generateDerivedStateValues = <State extends AgentStateNew>(state: State) => ({
121+
const generateDerivedStateValues = <State extends AgentState>(state: State) => ({
100122
isAvailable: (
101123
state === 'listening' ||
102124
state === 'thinking' ||
@@ -110,11 +132,11 @@ const useAgentTimeoutIdStore = create<{
110132
agentTimeoutFailureReason: string | null,
111133
startAgentTimeout: (agentConnectTimeoutMilliseconds?: number) => void;
112134
clearAgentTimeout: () => void;
113-
updateAgentTimeoutState: (agentState: AgentStateNew) => void;
135+
updateAgentTimeoutState: (agentState: AgentState) => void;
114136
updateAgentTimeoutParticipantExists: (agentParticipantExists: boolean) => void;
115137
subtle: {
116138
agentTimeoutId: ReturnType<typeof setTimeout> | null;
117-
agentState: AgentStateNew;
139+
agentState: AgentState;
118140
agentParticipantExists: boolean;
119141
};
120142
}>((set, get) => {
@@ -148,7 +170,7 @@ const useAgentTimeoutIdStore = create<{
148170
subtle: {
149171
...old.subtle,
150172
agentTimeoutId: startAgentConnectedTimeout(agentConnectTimeoutMilliseconds),
151-
agentState: 'unset',
173+
agentState: 'connecting',
152174
agentParticipantExists: false,
153175
},
154176
};
@@ -165,14 +187,14 @@ const useAgentTimeoutIdStore = create<{
165187
subtle: {
166188
...old.subtle,
167189
agentTimeoutId: null,
168-
agentState: 'unset',
190+
agentState: 'connecting',
169191
agentParticipantExists: false,
170192
},
171193
};
172194
});
173195
},
174196

175-
updateAgentTimeoutState: (agentState: AgentStateNew) => {
197+
updateAgentTimeoutState: (agentState: AgentState) => {
176198
set((old) => ({ ...old, subtle: { ...old.subtle, agentState: agentState } }));
177199
},
178200
updateAgentTimeoutParticipantExists: (agentParticipantExists: boolean) => {
@@ -181,7 +203,7 @@ const useAgentTimeoutIdStore = create<{
181203

182204
subtle: {
183205
agentTimeoutId: null,
184-
agentState: 'unset',
206+
agentState: 'connecting',
185207
agentParticipantExists: false,
186208
},
187209
};
@@ -305,7 +327,7 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
305327
return 'failed';
306328
}
307329

308-
let newState: AgentStateNew = 'unset';
330+
let newState: AgentState = 'connecting';
309331

310332
if (roomConnectionState !== ConnectionState.Disconnected) {
311333
newState = 'initializing';
@@ -318,8 +340,7 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
318340
}
319341

320342
if (agentParticipant && agentParticipantAttributes[ParticipantAgentAttributes.AgentState]) {
321-
// ref: https://github.com/livekit/agents/blob/65170238db197f62f479eb7aaef1c0e18bfad6e7/livekit-agents/livekit/agents/voice/events.py#L97
322-
const agentState = agentParticipantAttributes[ParticipantAgentAttributes.AgentState] as 'initializing' | 'idle' | 'listening' | 'thinking' | 'speaking';
343+
const agentState = agentParticipantAttributes[ParticipantAgentAttributes.AgentState] as AgentSdkStates;
323344
newState = agentState;
324345
}
325346

@@ -347,31 +368,6 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
347368
};
348369
}, [isConversationDisconnected, conversation.subtle.agentConnectTimeoutMilliseconds]);
349370

350-
const legacyAgentState: LegacyAgentState = useMemo(() => {
351-
switch (conversation.connectionState) {
352-
case ConnectionState.Disconnected:
353-
case ConnectionState.Connecting:
354-
return conversation.connectionState;
355-
356-
case ConnectionState.Connected:
357-
case ConnectionState.Reconnecting:
358-
case ConnectionState.SignalReconnecting:
359-
switch (state) {
360-
case 'speaking':
361-
case 'listening':
362-
case 'initializing':
363-
case 'thinking':
364-
return state;
365-
366-
case 'idle':
367-
case 'unset':
368-
case 'failed':
369-
// There's not really a good direct correlation for either of these...
370-
return 'disconnected';
371-
}
372-
}
373-
}, [conversation.connectionState, state]);
374-
375371
const agentState: AgentStateCases = useMemo(() => {
376372
const common: AgentInstanceCommon = {
377373
[Symbol.toStringTag]: "AgentInstance",
@@ -382,7 +378,6 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
382378
emitter,
383379
agentParticipant,
384380
workerParticipant,
385-
legacyAgentState,
386381
},
387382
};
388383

@@ -401,9 +396,20 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
401396
microphoneTrack: audioTrack,
402397
};
403398

404-
case 'unset':
405399
case 'initializing':
406400
case 'idle':
401+
return {
402+
...common,
403+
404+
state,
405+
...generateDerivedStateValues(state),
406+
failureReasons: null,
407+
408+
cameraTrack: videoTrack,
409+
microphoneTrack: audioTrack,
410+
};
411+
412+
case 'connecting':
407413
return {
408414
...common,
409415

@@ -446,7 +452,7 @@ export function useAgent(conversation: ConversationStub, _name?: string): AgentI
446452
}
447453

448454
return new Promise<void>((resolve, reject) => {
449-
const stateChangedHandler = (state: AgentStateNew) => {
455+
const stateChangedHandler = (state: AgentState) => {
450456
const { isAvailable } = generateDerivedStateValues(state);
451457
if (!isAvailable) {
452458
return;

packages/react/src/hooks/useVoiceAssistant.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,7 @@ import { useParticipantTracks } from './useParticipantTracks';
77
import { useTrackTranscription } from './useTrackTranscription';
88
import { useConnectionState } from './useConnectionStatus';
99
import { useParticipantAttributes } from './useParticipantAttributes';
10-
11-
/**
12-
* @beta
13-
*/
14-
export type AgentState =
15-
| 'disconnected'
16-
| 'connecting'
17-
| 'initializing'
18-
| 'listening'
19-
| 'thinking'
20-
| 'speaking';
10+
import { AgentState } from './useAgent';
2111

2212
/**
2313
* @beta

0 commit comments

Comments
 (0)